Que faire lorsque les tests TDD révèlent de nouvelles fonctionnalités nécessaires qui nécessitent également des tests?

13

Que faites-vous lorsque vous écrivez un test et que vous arrivez au point où vous devez réussir le test et que vous vous rendez compte que vous avez besoin d'une fonctionnalité supplémentaire qui devrait être séparée en sa propre fonction? Cette nouvelle fonction doit également être testée, mais le cycle TDD dit de faire échouer un test, le faire passer puis le refactoriser. Si je suis à l'étape où j'essaie de réussir mon test, je ne suis pas censé partir et commencer un autre test qui échoue pour tester la nouvelle fonctionnalité que j'ai besoin d'implémenter.

Par exemple, j'écris une classe de points qui a une fonction WillCollideWith ( LineSegment ) :

public class Point {
    // Point data and constructor ...

    public bool CollidesWithLine(LineSegment lineSegment) {
        Vector PointEndOfMovement = new Vector(Position.X + Velocity.X,
                                               Position.Y + Velocity.Y);
        LineSegment pointPath = new LineSegment(Position, PointEndOfMovement);
        if (lineSegment.Intersects(pointPath)) return true;
        return false;
    }
}

J'écrivais un test pour CollidesWithLine quand j'ai réalisé que j'aurais besoin d'une fonction LineSegment.Intersects ( LineSegment ) . Mais, dois-je simplement arrêter ce que je fais pendant mon cycle de test pour aller créer cette nouvelle fonctionnalité? Cela semble briser le principe "Rouge, Vert, Refactor".

Dois-je simplement écrire le code qui détecte cette intersection lineSegments à l'intérieur de la fonction CollidesWithLine et la refactoriser après qu'elle fonctionne? Cela fonctionnerait dans ce cas puisque je peux accéder aux données de LineSegment , mais qu'en est-il dans les cas où ce type de données est privé?

Joshua Harris
la source

Réponses:

14

Il suffit de commenter votre test et votre code récent (ou de le mettre dans une cachette) pour que vous ayez en fait remonté le temps au début du cycle. Commencez ensuite avec le LineSegment.Intersects(LineSegment)test / code / refactor. Une fois cela fait, décommentez votre test / code précédent (ou tirez de la cachette) et poursuivez le cycle.

Javier
la source
En quoi est-ce différent alors simplement l'ignorer et y revenir plus tard?
Joshua Harris
1
juste de petits détails: il n'y a pas de test supplémentaire "ignorez-moi" dans les rapports, et si vous utilisez des stashes, le code ne se distingue pas du cas "propre".
Javier
Qu'est-ce qu'une cachette? est-ce comme le contrôle de version?
Joshua Harris
1
certains VCS l'implémentent comme une fonctionnalité (au moins Git et Fossil). Il vous permet de supprimer un changement mais de l'enregistrer pour le réappliquer un peu plus tard. Ce n'est pas difficile à faire manuellement, enregistrez simplement un diff et revenez au dernier état. Plus tard, vous réappliquez le diff et continuez.
Javier
6

Sur le cycle TDD:

Dans la phase "faire passer le test", vous êtes censé écrire l'implémentation la plus simple qui fera passer le test . Pour réussir votre test, vous avez décidé de créer un nouveau collaborateur pour gérer la logique manquante car c'était peut-être trop de travail à mettre dans votre classe de points pour réussir votre test. C'est là que réside le problème. Je suppose que le test que vous tentiez de réussir était un trop grand pas . Je pense donc que le problème réside dans votre test lui-même, vous devez supprimer / commenter ce test et trouver un test plus simple qui vous permettra de faire un petit pas sans introduire la partie LineSegment.Intersects (LineSegment). Celui que vous avez réussi ce test, vous pouvez ensuite refactoriservotre code (ici vous appliquerez le principe SRP) en déplaçant cette nouvelle logique dans une méthode LineSegment.Intersects (LineSegment). Vos tests réussiront toujours parce que vous n'aurez changé aucun comportement mais simplement déplacé du code.

Sur votre solution de conception actuelle

Mais pour moi, vous avez un problème de conception plus profond ici, c'est que vous violez le principe de responsabilité unique . Le rôle d'un point est ... d'être un point, c'est tout. Il n'y a pas d'intelligence à être un point, c'est juste une valeur x et y. Les points sont des types de valeur . Il en va de même pour les segments, les segments sont des types de valeurs composés de deux points. Ils peuvent contenir un peu de "smartness" par exemple pour calculer leur longueur en fonction de la position de leurs points. Mais c'est tout.

Maintenant, décider si un point et un segment entrent en collision est une responsabilité à part entière. Et c'est certainement trop de travail pour un point ou un segment à gérer seul. Il ne peut pas appartenir à la classe Point, car sinon Points connaîtra les Segments. Et il ne peut pas appartenir aux segments car les segments ont déjà la responsabilité de prendre soin des points dans le segment et peut-être aussi de calculer la longueur du segment lui-même.

Cette responsabilité devrait donc appartenir à une autre classe comme par exemple un "PointSegmentCollisionDetector" qui aurait une méthode comme:

bool AreInCollision (Point p, Segment s)

Et c'est quelque chose que vous testeriez séparément à partir de points et de segments.

La bonne chose avec cette conception est que vous pouvez maintenant avoir une implémentation différente de votre détecteur de collision. Il serait donc facile par exemple de comparer votre moteur de jeu (je suppose que vous écrivez un jeu: p) en changeant votre méthode de détection de collision lors de l'exécution. Ou pour effectuer des vérifications / expériences visuelles lors de l'exécution entre différentes stratégies de détection de collision.

En ce moment, en mettant cette logique dans votre classe de points, vous bloquez les choses et poussez trop de responsabilité sur la classe de points.

J'espère que cela a du sens,

foobarcode
la source
Vous avez raison, j'essayais de tester un changement trop important et je pense que vous avez raison de séparer cela dans une classe de collision, mais cela me fait poser une toute nouvelle question que vous pourriez peut-être m'aider: devrais-je utiliser une interface lorsque les méthodes ne sont similaires? .
Joshua Harris
2

La chose la plus simple à faire de manière TDD serait d'extraire une interface pour LineSegment et de changer le paramètre de votre méthode pour prendre l'interface. Vous pouvez ensuite simuler le segment de ligne d'entrée et coder / tester la méthode Intersect indépendamment.

Dan Lyons
la source
1
Je sais que c'est la méthode TDD que j'entends le plus, mais un ILineSegment n'a pas de sens. C'est une chose d'interfacer une ressource externe ou quelque chose qui pourrait prendre de nombreuses formes, mais je ne vois pas une raison pour laquelle j'attacherais une quelconque fonctionnalité à autre chose qu'un segment de ligne.
Joshua Harris
0

Avec jUnit4, vous pouvez utiliser l' @Ignoreannotation pour les tests que vous souhaitez reporter.

Ajoutez l'annotation à chaque méthode que vous souhaitez reporter et continuez à écrire des tests pour la fonctionnalité requise. Encerclez pour refactoriser les cas de test plus anciens plus tard.

Bakoyaro
la source