Lors de l'exécution de TDD et de l'écriture d'un test unitaire, comment résister à l'envie de "tricher" lors de l'écriture de la première itération du code "d'implémentation" que vous testez?
Par exemple:
il faut que je calcule la factorielle d'un nombre. Je commence par un test unitaire (avec MSTest), par exemple:
[TestClass]
public class CalculateFactorialTests
{
[TestMethod]
public void CalculateFactorial_5_input_returns_120()
{
// Arrange
var myMath = new MyMath();
// Act
long output = myMath.CalculateFactorial(5);
// Assert
Assert.AreEqual(120, output);
}
}
J'exécute ce code, et il échoue car la CalculateFactorial
méthode n'existe même pas. J'écris donc maintenant la première itération du code pour implémenter la méthode sous test en écrivant le code minimum requis pour réussir le test.
Le problème, c’est que je suis toujours tenté d’écrire ce qui suit:
public class MyMath
{
public long CalculateFactorial(long input)
{
return 120;
}
}
Ceci est, sur le plan technique, correct en ce qu'elle vraiment est le code minimum requis pour faire ce test de passage spécifique (passer au vert), bien qu'il soit clairement un « tricher » , car il ne même pas vraiment tenter d'exécuter la fonction de calcul d' un factoriel. Bien entendu, la partie refactorisation devient désormais un exercice consistant à "écrire la fonctionnalité correcte" plutôt qu’une véritable refactorisation de la mise en oeuvre. Évidemment, l'ajout de tests supplémentaires avec différents paramètres échouera et forcera une refactorisation, mais vous devez commencer par ce test.
Ma question est donc la suivante: comment obtenez-vous cet équilibre entre "écrire le code minimum pour réussir le test" tout en le maintenant fonctionnel et dans l’esprit de ce que vous essayez réellement de réaliser?
la source
Réponses:
C'est parfaitement légitime. Rouge, vert, refactor.
Le premier test est réussi.
Ajoutez le deuxième test, avec une nouvelle entrée.
Maintenant passons rapidement au vert, vous pouvez ajouter un if-else, qui fonctionne bien. Cela passe, mais vous n'êtes pas encore fini.
La troisième partie de Red, Green, Refactor est la plus importante. Refactor pour supprimer la duplication . Vous aurez la duplication dans votre code maintenant. Deux instructions renvoyant des nombres entiers. Et le seul moyen de supprimer cette duplication consiste à coder correctement la fonction.
Je ne dis pas que vous ne l'écrivez pas correctement la première fois. Je dis juste que ce n'est pas tricher si vous ne le faites pas.
la source
Clairement, une compréhension de l'objectif ultime et la réalisation d'un algorithme répondant à cet objectif sont nécessaires.
Le TDD n’est pas une solution miracle au design; vous devez toujours savoir comment résoudre les problèmes en utilisant du code et savoir comment le faire à un niveau supérieur à quelques lignes de code pour réussir un test.
J'aime l'idée de TDD car elle encourage une bonne conception. cela vous fait penser à la façon dont vous pouvez écrire votre code pour qu'il soit testable, et généralement cette philosophie le poussera vers une meilleure conception globale. Mais vous devez encore savoir comment concevoir une solution.
Je ne suis pas en faveur des philosophies réductionnistes de TDD selon lesquelles il est possible de développer une application en écrivant simplement la plus petite quantité de code permettant de réussir un test. Sans penser à l'architecture, cela ne fonctionnera pas, et votre exemple le prouve.
Oncle Bob Martin dit ceci:
la source
Une très bonne question ... et je suis en désaccord avec presque tout le monde sauf @Robert.
L'écriture
pour une fonction factorielle, réussir un test est une perte de temps . Ce n'est pas "tricher", ni suivre le refactor rouge-vert littéralement. C'est faux .
Voici pourquoi:
les arguments du "refactor" sont erronés; si vous avez deux scénarios de test pour 5 et 6, ce code est toujours faux, car vous ne calculez pas de factorielle du tout :
si nous suivons à la lettre l' argument 'refactor' , alors, lorsque nous avons 5 cas de test, nous invoquons YAGNI et implémentons la fonction à l'aide d'une table de recherche:
Aucun d'entre eux ne calcule réellement quoi que ce soit, vous l'êtes . Et ce n'est pas la tâche!
la source
Lorsque vous avez écrit un seul test unitaire, l'implémentation sur une ligne (
return 120;
) est légitime. Écrire une boucle calculant la valeur de 120 - ce serait tricher!De tels tests initiaux simples sont un bon moyen de détecter les cas extrêmes et d'éviter les erreurs ponctuelles. Cinq n'est pas la valeur d'entrée avec laquelle je commencerais.
Une règle de base qui pourrait être utile ici est la suivante: zéro, un, plusieurs, beaucoup . Zéro et un sont des cas extrêmes pour la factorielle. Ils peuvent être mis en œuvre avec une seule ligne. Le "nombreux" cas de test (par exemple 5!) Vous obligerait alors à écrire une boucle. Le cas de test "lots" (1000!?) Peut vous obliger à mettre en œuvre un algorithme alternatif permettant de gérer des nombres très importants.
la source
factorial(5)
c'est un mauvais premier test. nous partons des cas les plus simples possibles et, à chaque itération, nous donnons aux tests un peu plus spécifiques, en incitant le code à devenir un peu plus générique. c'est ce que oncle bob appelle le principe de priorité de la transformation ( blog.8thlight.com/uncle-bob/2013/05/27/… )Tant que vous n'avez qu'un seul test, le code minimal requis pour réussir le test est vraiment
return 120;
, et vous pouvez le conserver facilement tant que vous n'avez plus de tests.Cela vous permet de différer la conception jusqu'à ce que vous écriviez réellement les tests qui exercent AUTRE valeurs renvoyées par cette méthode.
S'il vous plaît rappelez-vous que le test est la version exécutable de votre spécification, et si tout ce que cette spécification dit est que f (6) = 120, alors cela convient parfaitement.
la source
Si vous êtes capable de "tricher" de cette manière, cela suggère que vos tests unitaires sont imparfaits.
Plutôt que de tester la méthode factorielle avec une seule valeur, testez qu'il s'agit d'une plage de valeurs. Les tests basés sur les données peuvent aider ici.
Visualisez vos tests unitaires comme une manifestation des exigences. Ils doivent définir collectivement le comportement de la méthode testée. (Ceci est connu sous le nom de développement basé sur le comportement - c'est l'avenir
;-)
)Alors posez-vous la question suivante: si quelqu'un modifiait la mise en œuvre en une solution incorrecte, vos tests seraient-ils toujours satisfaisants ou diraient-ils "attendez une minute!"?
En gardant cela à l'esprit, si votre seul test était celui de votre question, techniquement, l'implémentation correspondante est correcte. Le problème est alors considéré comme une exigence mal définie.
la source
case
instructions à unswitch
, et vous ne pouvez pas écrire un test pour toutes les entrées et sorties possibles pour l'exemple du PO.Int64.MinValue
àInt64.MaxValue
. Cela prendrait beaucoup de temps, mais cela définirait explicitement l'exigence sans risque d'erreur. Avec la technologie actuelle, cela est impossible (je suppose que cela pourrait devenir plus commun à l'avenir) et je suis d'accord, vous pouvez tricher mais je pense que la question des PO n'était pas une question pratique (personne ne tricherait de cette manière en pratique), mais théorique.Il suffit d'écrire plus de tests. Finalement, il serait plus court d'écrire
que
:-)
la source
Écrire des tests de "triche" est correct, pour des valeurs suffisamment faibles de "OK". Mais rappelez-vous - les tests unitaires ne sont terminés que lorsque tous les tests sont réussis et qu'aucun nouveau test ne peut être écrit qui échouera . Si vous voulez vraiment avoir une méthode CalculateFactorial qui contient un tas d' instructions if (ou mieux encore, une grosse instruction switch / case :-) vous pouvez le faire, et puisque vous avez affaire à un nombre à précision fixe, le code requis sa mise en œuvre est finie (bien que probablement assez grande et laide, et peut-être limitée par les limitations du compilateur ou du système sur la taille maximale du code d'une procédure). À ce stade si vous vraimentinsistez sur le fait que tout développement doit être piloté par un test unitaire; vous pouvez écrire un test qui nécessite que le code calcule le résultat dans un délai inférieur à celui qui peut être obtenu en suivant toutes les branches de l' instruction if .
TDD peut vous aider à écrire du code qui implémente correctement les exigences , mais il ne peut pas vous forcer à écrire. bon code. C'est à toi de voir.
Partager et profiter.
la source
Je suis entièrement d'accord avec la suggestion de Robert Harveys: il ne s'agit pas uniquement de faire passer les tests, vous devez aussi garder à l'esprit l'objectif général.
En guise de solution à votre problème: "il est seulement vérifié de travailler avec un ensemble d'entrées donné", je vous proposerais d'utiliser des tests basés sur les données, tels que la théorie xunit. La puissance derrière ce concept est qu’il vous permet de créer facilement des spécifications d’entrées à sorties.
Pour Factorials, un test ressemblerait à ceci:
Vous pouvez même implémenter une fourniture de données de test (qui renvoie
IEnumerable<Tuple<xxx>>
) et coder un invariant mathématique, tel qu'une division répétée par n donnera n-1).Je trouve que c’est un moyen de test très puissant.
la source
Si vous êtes toujours capable de tricher, les tests ne suffisent pas. Ecrire plus de tests! Pour votre exemple, je vais essayer d’ajouter des tests avec les entrées 1, -1, -1000, 0, 10, 200.
Néanmoins, si vous vous engagez vraiment à tricher, vous pouvez écrire une infinité si-alors. Dans ce cas, rien ne pourrait aider sauf l'examen du code. Vous seriez bientôt pris sur le test d'acceptation ( écrit par une autre personne! )
Le problème avec les tests unitaires est parfois que les programmeurs les considèrent comme un travail inutile. La bonne façon de les voir est d’utiliser votre outil pour corriger le résultat de votre travail. Donc, si vous créez un si-alors, vous savez inconsciemment qu'il y a d'autres cas à considérer. Cela signifie que vous devez écrire un autre test. Et ainsi de suite jusqu'à ce que vous réalisiez que la tricherie ne fonctionne pas et qu'il vaut mieux coder de la bonne manière. Si vous sentez toujours que vous n'êtes pas fini, vous n'êtes pas fini.
la source
Je suggérerais que votre choix de test n'est pas le meilleur.
Je commencerais par:
factoriel (1) comme premier test,
factorielle (0) comme seconde
factoriel (-ve) comme troisième
puis continuez avec les cas non triviaux
et terminer avec un cas de débordement.
la source
-ve
??