Tous les exemples que j'ai lus et vus sur les vidéos de formation ont des exemples simplistes. Mais ce que je ne vois pas si je fais le "vrai" code après être devenu vert. Est-ce la partie "Refactor"?
Si j'ai un objet assez complexe avec une méthode complexe, et que j'écris mon test et le strict minimum pour le faire passer (après son premier échec, rouge). Quand dois-je revenir en arrière et écrire le vrai code? Et combien de code réel dois-je écrire avant de retester? Je suppose que le dernier est plus d'intuition.
Edit: Merci à tous ceux qui ont répondu. Toutes vos réponses m'ont énormément aidé. Il semble y avoir des idées différentes sur ce que je demandais ou ce qui m'embarrassait, et peut-être qu'il y en a, mais ce que je demandais était, disons que j'ai une demande pour construire une école.
Dans ma conception, j'ai une architecture avec laquelle je veux commencer, des User Stories, etc. De là, je prends ces User Stories et je crée un test pour tester la User Story. L'utilisateur dit: Nous avons des gens qui s'inscrivent à l'école et paient des frais d'inscription. Donc, je pense à un moyen de faire en sorte que cela échoue. Ce faisant, je conçois une classe de test pour la classe X (peut-être Student), qui échouera. Je crée ensuite la classe "Etudiant". Peut-être "l'école", je ne sais pas.
Mais, dans tous les cas, le design TD me force à réfléchir à l'histoire. Si je peux faire un le test échoue, je sais pourquoi il échoue, mais cela suppose que je puisse le faire passer. Il s'agit de la conception.
Je compare cela à la réflexion sur la récursivité. La récursivité n'est pas un concept difficile. Il peut être plus difficile de garder une trace de cela dans votre tête, mais en réalité, la partie la plus difficile est de savoir, quand la récursion "se brise", quand s'arrêter (à mon avis, bien sûr.) Donc je dois penser à ce qui s'arrête la récursion d'abord. Ce n'est qu'une analogie imparfaite, et elle suppose que chaque itération récursive est une "passe". Encore une fois, juste une opinion.
Dans la mise en œuvre, l'école est plus difficile à voir. Les livres numériques et bancaires sont "faciles" dans le sens où vous pouvez utiliser une arithmétique simple. Je peux voir a + b et retourner 0, etc. Dans le cas d'un système de personnes, je dois réfléchir davantage à la façon de mettre en œuvre cela. J'ai le concept de l'échec, de la réussite, de la refonte (principalement à cause de l'étude et de cette question.)
Ce que je ne sais pas est basé sur le manque d'expérience, à mon avis. Je ne sais pas comment échouer à inscrire un nouvel étudiant. Je ne sais pas comment faire échouer quelqu'un en tapant un nom de famille et qu'il soit enregistré dans une base de données. Je sais comment faire un + 1 pour les mathématiques simples, mais avec des entités comme une personne, je ne sais pas si je teste uniquement pour voir si je récupère un ID unique de base de données ou autre chose lorsque quelqu'un entre un nom dans un base de données ou les deux ou ni l'un ni l'autre.
Ou, peut-être que cela montre que je suis toujours confus.
Si j'ai un objet assez complexe avec une méthode complexe, et que j'écris mon test et le strict minimum pour le faire passer (après son premier échec, rouge). Quand dois-je revenir en arrière et écrire le vrai code? Et combien de code réel dois-je écrire avant de retester? Je suppose que le dernier est plus d'intuition.
Vous ne "retournez" pas et n'écrivez pas de "vrai code". C'est du vrai code. Ce que vous faites est de revenir en arrière et d'ajouter un autre test qui vous oblige à changer votre code afin de faire passer le nouveau test.
Quant à la quantité de code que vous écrivez avant de retester? Aucun. Vous écrivez zéro code sans un test d'échec qui force vous à écrire plus de code.
Remarquez le motif?
Passons en revue un (autre) exemple simple dans l'espoir que cela aide.
Assert.Equal("1", FizzBuzz(1));
Peazy facile.
public String FizzBuzz(int n) {
return 1.ToString();
}
Pas ce que vous appelleriez du vrai code, non? Ajoutons un test qui force un changement.
Assert.Equal("2", FizzBuzz(2));
Nous pourrions faire quelque chose de stupide comme if n == 1
, mais nous allons passer à la solution sensée.
public String FizzBuzz(int n) {
return n.ToString();
}
Cool. Cela fonctionnera pour tous les numéros non FizzBuzz. Quelle est la prochaine entrée qui forcera le code de production à changer?
Assert.Equal("Fizz", FizzBuzz(3));
public String FizzBuzz(int n) {
if (n == 3)
return "Fizz";
return n.ToString();
}
Et encore. Écrivez un test qui ne passera pas encore.
Assert.Equal("Fizz", FizzBuzz(6));
public String FizzBuzz(int n) {
if (n % 3 == 0)
return "Fizz";
return n.ToString();
}
Et nous avons maintenant couvert tous les multiples de trois (qui ne sont pas aussi des multiples de cinq, nous le noterons et reviendrons).
Nous n'avons pas encore écrit de test pour "Buzz", alors écrivons cela.
Assert.Equal("Buzz", FizzBuzz(5));
public String FizzBuzz(int n) {
if (n % 3 == 0)
return "Fizz";
if (n == 5)
return "Buzz"
return n.ToString();
}
Et encore une fois, nous savons qu'il y a un autre cas que nous devons gérer.
Assert.Equal("Buzz", FizzBuzz(10));
public String FizzBuzz(int n) {
if (n % 3 == 0)
return "Fizz";
if (n % 5 == 0)
return "Buzz"
return n.ToString();
}
Et maintenant, nous pouvons gérer tous les multiples de 5 qui ne sont pas également des multiples de 3.
Jusqu'à ce point, nous avons ignoré l'étape de refactorisation, mais je constate une certaine duplication. Nettoyons cela maintenant.
private bool isDivisibleBy(int divisor, int input) {
return (input % divisor == 0);
}
public String FizzBuzz(int n) {
if (isDivisibleBy(3, n))
return "Fizz";
if (isDivisibleBy(5, n))
return "Buzz"
return n.ToString();
}
Cool. Maintenant, nous avons supprimé la duplication et créé une fonction bien nommée. Quel est le prochain test que nous pouvons écrire qui nous obligera à changer le code? Eh bien, nous avons évité le cas où le nombre est divisible par 3 et 5. Écrivons-le maintenant.
Assert.Equal("FizzBuzz", FizzBuzz(15));
public String FizzBuzz(int n) {
if (isDivisibleBy(3, n) && isDivisibleBy(5, n))
return "FizzBuzz";
if (isDivisibleBy(3, n))
return "Fizz";
if (isDivisibleBy(5, n))
return "Buzz"
return n.ToString();
}
Les tests réussissent, mais nous avons plus de duplication. Nous avons des options, mais je vais appliquer "Extraire la variable locale" à quelques reprises afin de refactoriser au lieu de réécrire.
public String FizzBuzz(int n) {
var isDivisibleBy3 = isDivisibleBy(3, n);
var isDivisibleBy5 = isDivisibleBy(5, n);
if ( isDivisibleBy3 && isDivisibleBy5 )
return "FizzBuzz";
if ( isDivisibleBy3 )
return "Fizz";
if ( isDivisibleBy5 )
return "Buzz"
return n.ToString();
}
Et nous avons couvert toutes les entrées raisonnables, mais qu'en est-il de déraisonnable entrée? Que se passe-t-il si nous passons 0 ou un négatif? Écrivez ces cas de test.
public String FizzBuzz(int n) {
if (n < 1)
throw new InvalidArgException("n must be >= 1);
var isDivisibleBy3 = isDivisibleBy(3, n);
var isDivisibleBy5 = isDivisibleBy(5, n);
if ( isDivisibleBy3 && isDivisibleBy5 )
return "FizzBuzz";
if ( isDivisibleBy3 )
return "Fizz";
if ( isDivisibleBy5 )
return "Buzz"
return n.ToString();
}
Est-ce que cela commence à ressembler à du "vrai code"? Plus important encore, à quel moment a-t-il cessé d'être un "code irréel" et est-il devenu "réel"? C'est quelque chose à méditer ...
J'ai donc pu le faire simplement en recherchant un test que je savais ne pas réussir à chaque étape, mais j'ai eu beaucoup de pratique. Quand je suis au travail, les choses ne sont jamais aussi simples et je ne sais pas toujours quel test forcera un changement. Parfois, j'écrirai un test et serai surpris de voir qu'il passe déjà! Je vous recommande fortement de prendre l'habitude de créer une "liste de tests" avant de commencer. Cette liste de tests doit contenir toutes les entrées "intéressantes" auxquelles vous pouvez penser. Vous ne les utiliserez peut-être pas tous et vous ajouterez probablement des cas au fur et à mesure, mais cette liste sert de feuille de route. Ma liste de tests pour FizzBuzz ressemblerait à ceci.
Le "vrai" code est le code que vous écrivez pour réussir votre test. Vraiment . C'est si simple.
Lorsque les gens parlent d'écrire le strict minimum pour rendre le test vert, cela signifie simplement que votre vrai code doit suivre le principe YAGNI .
L'idée de l'étape de refactorisation est simplement de nettoyer ce que vous avez écrit une fois que vous êtes satisfait qu'il répond aux exigences.
Tant que les tests que vous écrivez englobent réellement les exigences de votre produit, une fois qu'ils ont réussi, le code est terminé. Pensez-y, si toutes les exigences de votre entreprise ont un test et que tous ces tests sont verts, que demander de plus? (D'accord, dans la vraie vie, nous n'avons pas tendance à avoir une couverture complète des tests, mais la théorie est solide.)
La réponse courte est que le "vrai code" est le code qui fait passer le test. Si vous pouvez réussir votre test avec autre chose que du vrai code, ajoutez plus de tests!
Je suis d'accord que beaucoup de tutoriels sur TDD sont simplistes. Cela fonctionne contre eux. Un test trop simple pour une méthode qui, par exemple, calcule 3 + 8 n'a vraiment pas d'autre choix que de calculer également 3 + 8 et de comparer le résultat. Cela donne l'impression que vous allez simplement dupliquer du code partout, et que les tests sont un travail supplémentaire inutile et sujet aux erreurs.
Lorsque vous êtes bon en test, cela vous informera de la façon dont vous structurez votre application et de la façon dont vous écrivez votre code. Si vous avez du mal à trouver des tests utiles et sensés, vous devriez probablement repenser un peu votre conception. Un système bien conçu est facile à tester - ce qui signifie que les tests sensibles sont faciles à penser et à mettre en œuvre.
Lorsque vous écrivez d'abord vos tests, regardez-les échouer, puis écrivez le code qui les fait passer, c'est une discipline pour vous assurer que tout votre code a des tests correspondants. Je ne suis pas servilement cette règle quand je code; souvent j'écris des tests après coup. Mais faire des tests d'abord vous aide à rester honnête. Avec une certaine expérience, vous commencerez à remarquer lorsque vous vous coderez dans un coin, même lorsque vous n'écrivez pas de tests en premier.
Parfois, certains exemples de TDD peuvent être trompeurs. Comme d'autres l'ont déjà fait remarquer, le code que vous écrivez pour réussir les tests est le vrai code.
Mais ne pensez pas que le vrai code apparaît comme de la magie - c'est faux. Vous avez besoin d'une meilleure compréhension de ce que vous voulez réaliser, puis vous devez choisir le test en conséquence, en commençant par les cas les plus faciles et les cas d'angle.
Par exemple, si vous avez besoin d'écrire un lexer, vous commencez avec une chaîne vide, puis avec un tas d'espaces blancs, puis un nombre, puis avec un nombre entouré d'espaces blancs, puis un mauvais numéro, etc. Ces petites transformations vous mèneront à le bon algorithme, mais vous ne passez pas du cas le plus simple à un cas très complexe choisi idiot pour obtenir le vrai code.
Bob Martin l'explique parfaitement ici .
La partie refactor est nettoyée lorsque vous êtes fatigué et que vous souhaitez rentrer chez vous.
Lorsque vous êtes sur le point d'ajouter une fonctionnalité, la partie refactor est ce que vous changez avant le prochain test. Vous refactorisez le code pour faire de la place pour la nouvelle fonctionnalité. Vous faites cela lorsque vous savez quelle sera cette nouvelle fonctionnalité. Pas quand vous l'imaginez.
Cela peut être aussi simple que de renommer GreetImpl
en GreetWorld
avant de créer une classe GreetMom
(après l'ajout d'un test) pour ajouter une fonction qui imprimera "Salut maman".
Vous écrivez Real Code tout le temps.
A chaque étape, vous écrivez du code pour satisfaire les conditions que votre code remplira pour les futurs appelants de votre code (qui peut être vous ou non ...).
Vous pensez que vous n'écrivez pas du code utile ( réel ), car dans un instant, vous pourriez le refactoriser.
Code-Refactoring est le processus de restructuration du code informatique existant — changer l'affacturage — sans changer son comportement externe.
Cela signifie que même si vous modifiez le code, les conditions de satisfaction du code restent inchangées. Et les vérifications ( tests ) Vous avez implémenté pour vérifier que votre code est déjà là pour vérifier si vos modifications ont changé quelque chose. Donc le code que vous avez écrit tout le temps est là, juste d'une manière différente.
Une autre raison pour laquelle vous pourriez penser que ce n'est pas du vrai code, c'est que vous faites des exemples où le programme final peut déjà être prévu par vous. C'est très bien, car cela montre que vous avez des connaissances sur le domaine dans lequel vous programmez.
Mais souvent, les programmeurs sont dans un domaine qui est nouveau , inconnu pour eux. Ils ne savent pas quel sera le résultat final et TDD est une technique pour écrire des programmes étape par étape, documentant notre connaissance du fonctionnement de ce système et vérification que notre code fonctionne de cette façon.
Lorsque j'ai lu Le Livre (*) sur TDD, pour moi, la caractéristique la plus importante qui s'est démarquée était la liste: TODO. Cela m'a montré que TDD est aussi une technique pour aider les développeurs à se concentrer sur une chose à la fois. C'est donc aussi une réponse à votre question à propos Combien de code réel écrire ? Je dirais assez de code pour se concentrer sur 1 chose à la fois.
(*) "Test Driven Development: By Example" par Kent Beck
Mais le vrai code apparaîtrait au stade du refactor de la phase TDD. C'est à dire. le code qui devrait faire partie de la version finale.
Les tests doivent être exécutés chaque fois que vous apportez une modification.
La devise du cycle de vie TDD serait: REFACTOR VERT ROUGE
[~ # ~] rouge [~ # ~] : Écrivez les tests
[~ # ~] vert [~ # ~] : Faites une tentative honnête pour obtenir le code fonctionnel qui passe les tests aussi rapidement que possible: code en double, obscurément hacks de variables nommées de premier ordre, etc.
Refacteur [~ # ~] [~ # ~] : Nettoyez le code, nommez correctement les variables. SEC le code.
Quand écrivez-vous le "vrai" code en TDD?
La phase rouge est l'endroit où vous écrivez le code .
Dans la phase refactoring le but principal est de supprimer le code.
Dans la phase rouge vous faites tout pour faire passer le test aussi vite que possible et à tout prix. Vous ignorez complètement ce que vous avez jamais entendu parler de bonnes pratiques de codage ou de modèle de conception. Faire passer le test au vert est tout ce qui compte.
Dans la phase refactoring vous nettoyez le désordre que vous venez de faire. Maintenant, regardez d'abord si la modification que vous venez de faire est le type le plus haut dans la liste de priorité de transformation et s'il y a une duplication de code, vous pouvez le supprimer le plus probablement en appliquant un motif de conception.
Enfin, vous améliorez la lisibilité en renommant les identificateurs et en extrayant nombres magiques et/ou des chaînes littérales en constantes.
Ce n'est pas du rouge-refactor, c'est du rouge-vert-refactor. - Rob Kinyon
Merci d'avoir signalé cela.
C'est donc la phase verte où vous écrivez le code réel
Dans la phase rouge vous écrivez la spécification exécutable ...
Vous n'écrivez pas de code pour faire échouer vos tests.
Vous écrivez vos tests pour définir à quoi devrait ressembler le succès, qui devrait tout d'abord échouer car vous n'avez pas encore écrit le code qui passera.
L'intérêt de l'écriture de tests initialement défaillants est de faire deux choses:
Le point derrière red-green-refactor est que l'écriture des tests corrects vous donne d'abord la confiance de savoir que le code que vous avez écrit pour passer les tests est correct, et vous permet de refactoriser avec la confiance que vos tests vous informeront dès que quelque chose se casse, vous pouvez donc immédiatement revenir en arrière et le réparer.
D'après ma propre expérience (C # /. NET), le premier test pur est un peu un idéal inaccessible, car vous ne pouvez pas compiler un appel à une méthode qui n'existe pas encore. Donc, "tester d'abord" consiste vraiment à coder les interfaces et les implémentations de stubbing d'abord, puis à écrire des tests sur les stubs (qui échoueront initialement) jusqu'à ce que les stubs soient correctement étoffés. Je n'écris jamais de "code défaillant", je construis simplement à partir de talons.
Je pense que vous pouvez être confondu entre les tests unitaires et les tests d'intégration. Je pense qu'il peut également y avoir des tests d'acceptation, mais cela dépend de votre processus.
Une fois que vous avez testé toutes les petites "unités", vous les testez toutes assemblées ou "intégrées". C'est généralement un programme entier ou une bibliothèque.
Dans le code que j'ai écrit, les tests d'intégration une bibliothèque avec divers programmes de test qui lisent les données et les alimentent à la bibliothèque, puis vérifiez les résultats. Ensuite, je le fais avec des fils. Ensuite, je le fais avec des fils et une fourche () au milieu. Ensuite, je l'exécute et tue -9 après 2 secondes, puis je le démarre et vérifie son mode de récupération. Je le floue. Je le torture de toutes sortes de façons.
Tout cela est également un test, mais je n'ai pas un joli affichage rouge/vert pour les résultats. Il réussit ou je fouille quelques milliers de lignes de code d'erreur pour savoir pourquoi.
C'est là que vous testez le "vrai code".
Et je viens de penser à ça, mais peut-être que vous ne savez pas quand vous êtes censé avoir terminé les tests unitaires. Vous avez terminé d'écrire des tests unitaires lorsque vos tests exercent tout ce que vous avez spécifié. Parfois, vous pouvez perdre la trace de cela parmi tous les cas de gestion des erreurs et Edge, alors vous voudrez peut-être créer un groupe de tests Nice de tests de chemin heureux qui passent simplement par les spécifications.