Comment les personnes qui utilisent TDD gèrent-elles la perte de travail lors d’une refactorisation majeure?

37

Cela fait un moment que j'essaie d'apprendre à écrire des tests unitaires pour mon code.

Au départ, j’ai commencé à faire du vrai TDD, où je n’écrivais aucun code tant que j’avais écrit un test d’échec en premier.

Cependant, j'ai récemment eu un problème épineux à résoudre qui impliquait beaucoup de code. Après avoir passé quelques bonnes semaines à écrire des tests, puis du code, j’en suis arrivée à la conclusion malheureuse que mon approche n’allait pas aboutir et que je devrais jeter deux semaines de travail et recommencer.

C'est une décision assez mauvaise à prendre lorsque vous venez d'écrire le code, mais lorsque vous avez également écrit plusieurs centaines de tests unitaires, il devient encore plus difficile émotionnellement de tout gâcher.

Je ne peux pas m'empêcher de penser que j'ai perdu 3 ou 4 jours d'effort à rédiger ces tests alors que j'aurais pu simplement rassembler le code pour la validation technique et ensuite les rédiger par la suite, une fois que j'étais satisfait de mon approche.

Comment les personnes qui pratiquent le TDD gèrent-elles correctement ces situations? Y a-t-il lieu de modifier les règles dans certains cas ou écrivez-vous toujours servilement d'abord les tests, même si ce code peut s'avérer inutile?

GazTheDestroyer
la source
6
La perfection est atteinte, non pas lorsqu'il n'y a plus rien à ajouter, mais lorsqu'il ne reste plus rien à enlever. - Antoine de Saint-Exupery
mouviciel le
12
Comment est-il possible que tous vos tests soient erronés? Veuillez expliquer en quoi un changement d’ implémentation invalide chaque test que vous avez écrit.
S.Lott
6
@ S.Lott: Les tests n'étaient pas faux, ils n'étaient tout simplement plus pertinents. Supposons que vous résolvez une partie du problème à l'aide de nombres premiers. Vous devez donc écrire une classe pour générer des nombres premiers et écrire des tests pour cette classe afin de vous assurer de son bon fonctionnement. Maintenant, vous trouvez une autre solution totalement différente à votre problème qui n’implique aucunement les nombres premiers. Cette classe et ses tests sont maintenant redondants. C’était ma situation avec seulement 10 classes et pas seulement une.
GazTheDestroyer
5
@GazTheDestroyer sa me semble que distinguer le code de test du code fonctionnel est une erreur - tout cela fait partie du même processus de développement. Il est juste de noter que TDD a des frais généraux qui sont généralement récupérés plus tard au cours du processus de développement, et il semble que ces frais généraux ne vous ont rien apporté dans ce cas. Mais également dans quelle mesure les tests ont-ils éclairé votre compréhension des défaillances de l'architecture? Il est également important de noter que vous êtes autorisé (non, encouragé ) à modifier vos tests au fil du temps ... bien que ceci soit probablement un peu extrême (-:
Murph
10
Je vais être sémantiquement pédant et être d'accord avec @ S.Lott ici; ce que vous avez fait n’est pas une refactorisation si cela aboutit à jeter de nombreuses classes et leurs tests. C'est une nouvelle architecture . La refactorisation, en particulier dans le sens TDD, signifie que les tests étaient verts, que vous avez modifié un code interne, que vous avez réexécuté les tests et qu'ils sont restés verts.
Eric King

Réponses:

33

Je pense qu'il y a deux problèmes ici. La première est que vous n'avez pas compris à l'avance que votre conception d'origine n'était peut-être pas la meilleure approche. Si vous aviez su cela à l'avance, vous auriez peut-être choisi de développer un ou deux prototypes à jeter , d'explorer les options de conception possibles et d'évaluer le moyen le plus prometteur à suivre. En prototypage, vous n’avez pas besoin d’écrire un code de qualité de production ni d’essais unitaires dans chaque recoin (ou pas du tout), car vous vous concentrez uniquement sur l’apprentissage, pas sur le polissage du code.

Maintenant, se rendre compte que vous avez besoin de prototypage et d'expériences plutôt que de commencer immédiatement le développement de code de production, n'est pas toujours facile, ni même toujours possible. Armés des connaissances que vous venez d'acquérir, vous pourrez peut-être reconnaître la nécessité du prototypage la prochaine fois. Ou peut-être pas. Mais au moins, vous savez maintenant que cette option doit être envisagée. Et ceci en soi est une connaissance importante.

L'autre problème est à mon humble avis avec votre perception. Nous faisons tous des erreurs, et il est si facile de voir rétrospectivement ce que nous aurions dû faire différemment. C'est comme ça que nous apprenons. Notez votre investissement dans les tests unitaires, car il vous faudra apprendre que le prototypage peut être important et surmontez-le. Efforcez-vous simplement de ne pas commettre deux fois la même erreur :-)

Péter Török
la source
2
Je savais que le problème serait difficile à résoudre et que mon code serait quelque peu exploratoire, mais j'étais enthousiasmé par mes récents succès en TDD, j'ai donc continué à écrire des tests comme je l'avais fait depuis la littérature TDD insiste beaucoup. Alors oui, maintenant que je sais que les règles peuvent être enfreintes (ce qui était vraiment ma question), je vais probablement mettre cela au compte-rendu.
GazTheDestroyer
3
"J'ai continué à écrire des tests comme je le faisais depuis, c'est ce que toute la littérature de TDD insiste tant sur les choses". Vous devriez probablement mettre à jour la question avec la source de votre idée selon laquelle tous les tests doivent être écrits avant tout code.
S.Lott
1
Je n'ai pas cette idée et je ne suis pas sûr de savoir comment vous avez tiré cela du commentaire.
GazTheDestroyer
1
J'allais écrire une réponse, mais j'ai voté à la place avec la vôtre. Oui, un million de fois, oui: si vous ne savez pas encore à quoi ressemble votre architecture, écrivez d’abord un prototype jetable et ne vous embêtez pas à écrire des tests unitaires lors du prototypage.
Robert Harvey
1
@WarrenP, il y a sûrement des gens qui pensent que le TDD est la seule vraie façon (tout peut être transformé en religion si vous essayez assez fort ;-). Je préfère cependant être pragmatique. Pour moi, TDD est l'un des outils de ma boîte à outils, et je ne l'utilise que lorsqu'il est utile, au lieu d'empêcher, la résolution de problèmes.
Péter Török
8

Le but de TDD est qu’il vous oblige à écrire de petits incréments de code dans de petites fonctions , précisément pour éviter ce problème. Si vous avez passé des semaines à écrire du code sur un domaine et que chaque méthode utilitaire que vous avez écrite devient inutile lorsque vous repensez l'architecture, vos méthodes sont certainement beaucoup trop volumineuses au départ. (Oui, je sais que ce n'est pas vraiment réconfortant maintenant ...)

Kilian Foth
la source
3
Mes méthodes n'étaient pas du tout grandes, elles sont simplement devenues inutiles étant donné la nouvelle architecture qui ne ressemblait en rien à l'ancienne. En partie parce que la nouvelle architecture était beaucoup plus simple.
GazTheDestroyer
Très bien, s'il n'y a vraiment rien de réutilisable, vous ne pouvez que réduire vos pertes et passer à autre chose. Mais la promesse de TDD est de vous permettre d’atteindre plus rapidement les mêmes objectifs, même si vous écrivez du code de test en plus du code de l’application. Si cela est vrai et je le crois fermement, vous avez au moins atteint le point où vous avez compris comment faire l'architecture en deux semaines plutôt que deux fois.
Kilian Foth
1
@Kilian, re "La promesse du TDD est de vous permettre d'atteindre plus rapidement les mêmes objectifs" - de quels objectifs parlez-vous ici? Il est assez évident que l'écriture de tests unitaires avec le code de production lui-même vous ralentisse au départ , comparé à la simple production de code. Je dirais que TDD ne remboursera que sur le long terme, en raison de l'amélioration de la qualité et de la réduction des coûts de maintenance.
Péter Török
@ PéterTörök - Il y a des gens qui insistent sur le fait que TDD n'a jamais de coût, car il est rentabilisé lorsque vous avez écrit le code. Ce n'est certainement pas le cas pour moi, mais Killian semble le croire par lui-même.
psr
Eh bien ... si vous ne croyez pas cela, en fait, si vous ne croyez pas que TDD génère un gain substantiel plutôt qu'un coût, alors il est inutile de le faire du tout, n'est-ce pas? Pas seulement dans la situation très spécifique décrite par Gaz, mais du tout . J'ai bien peur d'avoir conduit ce fil complètement hors-sujet :(
Kilian Foth le
6

Brooks a dit "prévoyez d'en jeter un, vous le ferez de toute façon". Il me semble que c'est ce que vous faites. Cela dit, vous devriez écrire vos tests unitaires pour tester une unité de code et non une grande bande de code. Ce sont des tests plus fonctionnels et devraient donc sur toute implémentation interne.

Par exemple, si je veux écrire un solveur PDE (équations aux dérivées partielles), j'écrirai quelques tests pour tenter de résoudre des problèmes que je peux résoudre mathématiquement. Ce sont mes premiers tests unitaires - lisez: les tests fonctionnels sont exécutés dans le cadre d’un framework xUnit. Celles-ci ne changeront pas en fonction de l'algorithme que j'utilise pour résoudre le PDE. Tout ce qui m'importe, c'est le résultat. Les deuxièmes tests unitaires se concentreront sur les fonctions utilisées pour coder l'algorithme et seraient donc spécifiques à cet algorithme - par exemple, Runge-Kutta. Si je découvrais que Runge-Kutta ne convient pas, j'aurais toujours ces tests de haut niveau (y compris ceux qui ont montré que Runge-Kutta n'était pas approprié). Ainsi, la deuxième itération aurait encore beaucoup des mêmes tests que la première.

Votre problème est peut-être de conception et pas nécessairement de code. Mais sans plus de détails, c'est difficile à dire.

Sardathrion - Rétablir Monica
la source
C'est seulement périphérique, mais qu'est-ce que c'est l'EDP?
un CVn
1
@ MichaelKjörling Je suppose que c'est l'équation différentielle partielle
foraidt
2
Brooks n’a-t-il pas retiré cette déclaration dans sa deuxième édition?
Simon
Comment voulez-vous dire que vous aurez toujours les tests qui montrent que Runge-Kutta n'était pas adapté? À quoi ressemblent ces tests? Voulez-vous dire que vous avez sauvegardé l'algorithme Runge-Kutta que vous avez écrit avant de découvrir qu'il ne convenait pas et que l'exécution des tests de bout en bout avec RK dans le mix échouerait?
moteutsch
5

N'oubliez pas que le TDD est un processus itératif. Ecrivez un petit test (dans la plupart des cas, quelques lignes devraient suffire) et exécutez-le. Le test devrait échouer. Maintenant, travaillez directement sur votre source principale et essayez d'implémenter la fonctionnalité testée pour que le test réussisse. Maintenant, recommencez.

Vous ne devez pas essayer d’écrire tous les tests en une fois, car, comme vous l’avez remarqué, cela ne va pas aboutir. Cela réduit le risque de perdre votre temps à rédiger des tests inutilisés.

BenR
la source
1
Je ne pense pas pouvoir m'expliquer très bien. J'écris des tests de manière itérative. C'est ainsi que j'ai abouti à plusieurs centaines de tests de code qui sont soudainement devenus redondants.
GazTheDestroyer
1
Comme ci-dessus - Je pense que cela devrait être considéré comme "des tests et du code" plutôt que "des tests de code"
Murph le
1
+1: "Vous ne devriez pas essayer d'écrire tous les tests en une fois"
S.Lott
4

Je pense que vous l'avez dit vous-même: vous n'étiez pas sûr de votre approche avant de commencer à écrire tous vos tests unitaires.

Ce que j’ai appris en comparant les projets TDD réels avec lesquels j’ai travaillé (pas beaucoup en réalité, mais seulement trois couvrant deux années de travail) avec ce que j’avais appris théoriquement, c’est que les tests automatisés! = Les tests unitaires exclusif).

En d'autres termes, le T dans TDD ne doit pas nécessairement comporter un U ... Il est automatisé, mais est moins un test unitaire (comme dans les classes et méthodes de test) qu'un test fonctionnel automatisé: il est au même niveau. de granularité fonctionnelle selon l’architecture sur laquelle vous travaillez actuellement. Vous commencez de haut niveau, avec peu de tests et seulement une vue d'ensemble fonctionnelle, et vous finissez par vous retrouver avec des milliers d'UT et toutes vos classes bien définies dans une belle architecture ...

Les tests unitaires vous apportent une aide précieuse lorsque vous travaillez en équipe, pour éviter les modifications de code générant des cycles de bogues sans fin. Mais je n'ai jamais rien écrit d'aussi précis lorsque je commence à travailler sur un projet, avant d'avoir au moins un POC de travail global pour chaque user story.

Peut-être que c'est juste ma façon personnelle de le faire. Je n'ai pas l'expérience suffisante pour décider à partir de rien les structures ou la structure de mon projet, je ne perdrai donc pas mon temps à écrire des centaines d'UT depuis le début ...

Plus généralement, l’idée de tout casser et de tout jeter sera toujours là. Aussi «continus» que nous pouvons essayer d’être avec nos outils et méthodes, la seule façon de lutter contre l’entropie est parfois de tout recommencer. Mais le but est que, lorsque cela se produira, les tests automatisés et les tests unitaires que vous avez mis en œuvre rendront votre projet déjà moins coûteux que s’il n’y en avait pas - et il le fera si vous trouvez l’équilibre.

GFK
la source
3
bien dit - c'est TDD, pas UTDD
Steven A. Lowe
Excellente réponse. D'après mon expérience du TDD, il est important que les tests écrits se concentrent sur les comportements fonctionnels du logiciel et s'éloignent des tests unitaires. Il est plus difficile de penser aux comportements que vous attendez d'une classe, mais cela conduit à des interfaces propres et simplifie potentiellement l'implémentation résultante (vous n'ajoutez pas de fonctionnalités dont vous n'avez pas réellement besoin).
JohnTESlade
4
Comment les personnes qui pratiquent le TDD gèrent-elles correctement ces situations?
  1. en considérant quand prototyper vs quand coder
  2. en réalisant que les tests unitaires ne sont pas les mêmes que TDD
  3. par écrit TDD teste pour vérifier une caractéristique / histoire, pas une unité fonctionnelle

La fusion des tests unitaires avec le développement piloté par les tests est la source de beaucoup d'angoisse et de malheurs. Revenons donc une fois de plus:

  • les tests unitaires consistent à vérifier chaque module individuel et chaque fonction dans la mise en œuvre ; dans UT, vous verrez une emphase sur des choses comme les métriques de couverture de code et les tests qui s'exécutent très rapidement
  • le développement piloté par les tests concerne la vérification de chaque fonctionnalité / récit dans les exigences ; Dans TDD, l'accent est mis sur des tâches telles que l'écriture du test en premier, en veillant à ce que le code écrit n'excède pas la portée prévue et le refactoring pour la qualité.

En résumé: les tests unitaires sont axés sur la mise en œuvre, TDD sur les exigences. Ce n'est pas la même chose.

Steven A. Lowe
la source
"TDD met l'accent sur les exigences" Je ne suis absolument pas d'accord avec cela. Les tests que vous écrivez en TDD sont des tests unitaires. Ils font vérifier chaque fonction / méthode. TDD fait avoir un accent sur la couverture de code et ne se soucier de tests qui exécutent rapidement (et ils feraient mieux de faire, puisque vous exécutez les tests toutes les 30 secondes environ ). Peut-être que vous pensiez à ATDD ou à BDD?
guillaume31
1
@ ian31: exemple parfait de la fusion entre UT et TDD. Doit être en désaccord et vous renvoyer à certains documents sources en.wikipedia.org/wiki/Test-driven_development - les tests ont pour but de définir les exigences du code . BDD est génial. Jamais entendu parler d'ATDD, mais en un coup d'œil, je vois comment j'applique la mise à l'échelle TDD .
Steven A. Lowe
Vous pouvez parfaitement utiliser TDD pour concevoir un code technique qui n'est pas directement lié à une exigence ou à une user story. Vous trouverez d'innombrables exemples de cela sur le Web, dans des livres, des conférences, y compris ceux qui ont initié le TDD et l'ont popularisé. TDD est une discipline, une technique pour écrire du code, il ne cessera pas d’être TDD en fonction du contexte dans lequel vous l’utilisez.
guillaume31
En outre, dans l'article Wikipedia que vous avez mentionné: "Les pratiques avancées de développement piloté par les tests peuvent conduire à ATDD où les critères spécifiés par le client sont automatisés en tests d'acceptation, lesquels pilotent ensuite le processus traditionnel de développement piloté par tests unitaires (UTDD). [ ...] Avec ATDD, l’équipe de développement a maintenant un objectif spécifique à satisfaire, les tests d’acceptation, qui les garde constamment concentrés sur ce que le client veut vraiment de cette histoire d’utilisateur. " Ce qui semble impliquer qu'ATDD est principalement axé sur les exigences, et non sur le TDD (ou l'UTDD, comme ils le disent).
guillaume31
@ ian31: La question de l'OP sur le «rejet de plusieurs centaines de tests unitaires» indiquait une confusion d'échelle. Vous pouvez utiliser TDD pour construire un hangar si vous le souhaitez. : D
Steven A. Lowe
3

Le développement piloté par les tests est destiné à guider votre développement. Les tests que vous écrivez vous permettent d’affirmer l’exactitude du code que vous écrivez et d’accroître la vitesse de développement à partir de la première ligne.

Vous semblez croire que les tests sont un fardeau et ne sont conçus que pour un développement progressif plus tard. Cette ligne de pensée ne correspond pas à la TDD.

Peut-être pouvez-vous le comparer au typage statique: bien que vous puissiez écrire du code sans information de type statique, l'ajout de type statique au code aide à affirmer certaines propriétés du code, libérant ainsi l'esprit et permettant de se concentrer sur la structure importante, augmentant ainsi la vitesse et efficacité.

Dibbeke
la source
2

Le problème avec une refactorisation majeure est que vous pouvez et allez parfois suivre un chemin qui vous amène à réaliser que vous avez piqué plus que vous ne pouvez en mâcher. Les refactorisations géantes sont une erreur. Si la conception du système est défectueuse au départ, la refactorisation peut vous mener loin avant que vous deviez prendre une décision difficile. Vous pouvez soit laisser le système en l'état et contourner le problème, soit planifier de revoir la conception et d'apporter des modifications majeures.

Il y a cependant une autre façon. Le véritable avantage du code de refactoring est de rendre les choses plus simples, plus lisibles et encore plus faciles à maintenir. Lorsque vous abordez un problème pour lequel vous avez des doutes, vous indiquez un changement, allez jusqu’à voir où il pourrait conduire afin d’en savoir plus sur le problème, puis jetez la pointe et appliquez une nouvelle refactorisation en fonction de la pointe. vous a appris. Le fait est que vous ne pouvez vraiment améliorer votre code avec certitude que si les étapes sont petites et si vos efforts de refactoring ne surchargent pas votre capacité à écrire vos tests en premier. La tentation est d'écrire un test, puis du code, puis du code supplémentaire, car une solution peut sembler évidente, mais vous vous rendez vite compte que votre modification modifiera de nombreux autres tests. Vous devez donc faire attention à ne changer qu'une seule chose à la fois.

La solution est donc de ne jamais faire de votre refactoring un projet majeur. Pas de bébé. Commencez par extraire les méthodes, puis cherchez à supprimer les doublons. Passez ensuite à l'extraction des classes. Chacune par petites étapes, un changement mineur à la fois. SI vous extrayez du code, écrivez d'abord un test. Si vous supprimez du code, supprimez-le, lancez vos tests et déterminez si l'un des tests interrompus sera nécessaire. Un petit pas de bébé à la fois. Il semble que cela prendra plus de temps, mais réduira considérablement votre temps de refactoring.

La réalité est cependant que chaque pointe représente apparemment un gaspillage d’efforts. Les modifications de code ne vont parfois nulle part, et vous vous retrouvez en train de restaurer votre code depuis votre vcs. Ceci est juste une réalité de ce que nous faisons au jour le jour. Chaque pointe qui échoue n'est pas perdue cependant, si elle vous apprend quelque chose. Chaque effort de refactoring qui échoue vous apprendra que vous essayez de faire trop, trop vite ou que votre approche est peut-être fausse. Cela aussi n’est pas une perte de temps si vous en tirez des leçons. Plus vous en ferez, plus vous en apprendrez et plus vous deviendrez efficace. Mon conseil est de simplement le porter pour le moment, d'apprendre à faire plus en faisant moins et d'accepter que c'est exactement ce qu'il faut faire jusqu'à ce que vous sachiez mieux jusqu'où prendre une pointe avant qu'elle ne vous mène nulle part.

S.Robins
la source
1

Je ne suis pas sûr de la raison pour laquelle votre approche s'est révélée imparfaite après 3 jours. En fonction de vos incertitudes dans votre architecture, vous pouvez envisager de modifier votre stratégie de test:

  • Si vous avez des doutes sur les performances, vous souhaitez peut-être commencer par quelques tests d’intégration permettant d’affirmer les performances?

  • Lorsque vous étudiez la complexité de l'API, écrivez de véritables tests unitaires simples et petits pour déterminer quelle serait la meilleure façon de procéder. Ne vous embêtez pas dans la mise en œuvre, demandez simplement à vos classes de renvoyer des valeurs codées en dur ou de leur donner des exceptions NotImplementedExceptions.

Boris Callens
la source
0

Pour moi, les tests unitaires sont également une occasion de mettre l'interface sous une utilisation "réelle" (enfin, aussi réels que soient les tests unitaires!).

Si je suis obligé de mettre en place un test, je dois exercer mon design. Cela aide à garder les choses saines (si quelque chose est si complexe que le test est un fardeau, qu'en sera-t-il de l'utiliser?).

Cela n'évite pas les modifications de conception, mais en expose le besoin. Oui, une réécriture complète est une douleur. Pour (essayer de) l'éviter, j'ai l'habitude de mettre en place un (plusieurs) prototype (s), éventuellement en Python (avec le développement final en c ++).

Certes, vous n'avez pas toujours le temps pour tous ces cadeaux. Ce sont précisément les cas où vous aurez besoin d' un LARGER de temps pour atteindre vos objectifs ... et / ou de tout garder sous contrôle.

Francesco
la source
0

Bienvenue au cirque des développeurs créatifs .


Au lieu de respecter tous les codes «légaux / raisonnables» de code au début,
essayez l’ intuition , surtout si elle est importante et nouvelle pour vous et si aucun échantillon n’a l’air de vouloir:

- Écrivez avec votre instinct, à partir de choses que vous connaissez déjà , pas avec votre mental et votre imagination.
- Et arrêtez.
- Prenez une loupe et inspectez tous les mots que vous écrivez: Vous écrivez "texte" car "texte" est proche de String, mais "verbe", "adjectif" ou quelque chose de plus précis est nécessaire, relisez et ajustez la méthode avec un nouveau sens
. .. ou vous avez écrit un morceau de code en pensant au futur? supprimez-le
- Corrigez, effectuez une autre tâche (sport, culture ou autre chose en dehors de l'entreprise), revenez et relisez.
- Tout va bien,
- Corrigez, faites une autre tâche, revenez et relisez.
- Tout va bien, passez à TDD
- Maintenant, tout est correct, bon
- Essayez le point de repère pour indiquer les choses à optimiser, faites-le.

Ce qui apparaît:
- vous avez écrit un code respectant toutes les règles
- vous obtenez une expérience, une nouvelle façon de travailler,
- quelque chose change dans votre esprit, vous n’aurez pas peur de la nouvelle configuration.

Et maintenant, si vous voyez un fichier UML ressemblant à ce qui précède, vous pourrez dire:
"Boss, je commence par TDD pour cela ...."
est-ce une autre chose nouvelle?
"Boss, je voudrais essayer quelque chose avant de décider de la façon dont je vais coder."

Cordialement, PARIS
Claude

cl-r
la source