Devrions-nous concevoir notre code dès le début pour permettre les tests unitaires?

91

Notre équipe se demande actuellement si la modification de la conception du code pour permettre les tests unitaires est une odeur de code, ou dans quelle mesure cela peut être fait sans être une odeur de code. Cela est dû au fait que nous commençons tout juste à mettre en place des pratiques qui sont présentes dans à peu près toutes les autres sociétés de développement de logiciels.

Plus précisément, nous aurons un service API Web qui sera très mince. Sa principale responsabilité consistera à organiser les requêtes / réponses Web et à appeler une API sous-jacente contenant la logique métier.

Un exemple est que nous prévoyons de créer une usine qui renverra un type de méthode d'authentification. Nous n'avons pas besoin qu'il hérite d'une interface car nous ne prévoyons pas qu'il s'agira jamais d'un type autre que concret. Cependant, pour tester un peu le service API Web, nous devrons nous moquer de cette usine.

Cela signifie essentiellement que nous concevons la classe de contrôleur API Web pour accepter DI (via son constructeur ou son séparateur), ce qui signifie que nous concevons une partie du contrôleur simplement pour permettre à DI et implémenter une interface dont nous n'avons pas autrement besoin, ou nous utilisons Un framework tiers, comme Ninject, évite de devoir concevoir le contrôleur de cette manière, mais il reste à créer une interface.

Certains membres de l'équipe semblent réticents à concevoir du code uniquement pour tester. Il me semble qu’il faut trouver un compromis pour espérer passer le test unitaire, mais je ne sais pas comment répondre à leurs préoccupations.

Soyons clairs: il s’agit d’un tout nouveau projet. Il ne s’agit donc pas vraiment de modifier le code pour permettre les tests unitaires; il s'agit de concevoir le code que nous allons écrire pour être unitaire testable.

Lee
la source
33
Permettez-moi de répéter ceci: vos collègues veulent des tests unitaires pour le nouveau code, mais ils refusent d'écrire le code de manière à ce qu'il soit testable à l'unité, bien qu'il n'y ait aucun risque à casser quelque chose d'existant? Si cela est vrai, vous devriez accepter la réponse de @ KilianFoth et lui demander de mettre en surbrillance la première phrase de sa réponse en gras! Vos collègues ont apparemment un très grand malentendu sur leur travail.
Doc Brown le
20
@ Lee: Qui a dit que le découplage est toujours une bonne idée? Avez-vous déjà vu une base de code dans laquelle tout est passé sous forme d'interface créée à partir d'une fabrique d'interfaces utilisant une interface de configuration? J'ai; c'était écrit en Java et c'était un gâchis buggy complet, intenable. Le découplage extrême est une obfuscation de code.
Christian Hackl
8
Le travail efficace de Michael Feathers avec Legacy Code résout très bien ce problème et devrait vous donner une bonne idée des avantages du test, même dans une nouvelle base de code.
l0b0
8
@ l0b0 C'est à peu près la bible pour cela. Sur stackexchange, ce ne serait pas une réponse à la question, mais dans RL, je dirais à OP de lire ce livre (au moins en partie). OP, obtenir Travailler efficacement avec Legacy Code et lu, au moins en partie (ou dire à votre patron pour l' obtenir). Il aborde des questions comme celles-ci. Surtout si vous n’avez pas fait d’essais et que vous vous lancez dans l’épreuve - vous avez peut-être 20 ans d’expérience, mais vous allez maintenant faire des choses que vous n’avez pas expérimentées . Il est tellement plus facile de lire sur eux que d'apprendre minutieusement tout cela par essais et erreurs.
R. Schmitz
4
Merci pour la recommandation du livre de Michael Feathers, je vais certainement en prendre un exemplaire.
Lee

Réponses:

204

La réticence à modifier le code à des fins de test montre qu'un développeur n'a pas compris le rôle des tests et, par voie de conséquence, leur propre rôle dans l'organisation.

Le secteur des logiciels s’articule autour de la fourniture d’une base de code qui crée de la valeur commerciale. Nous avons constaté, grâce à notre longue et amère expérience, que nous ne pouvons pas créer de telles bases de code de taille non triviale sans tests. Par conséquent, les suites de tests font partie intégrante de l'entreprise.

Beaucoup de codeurs ne tiennent aucun compte de ce principe mais ne l’acceptent jamais inconsciemment. Il est facile de comprendre pourquoi. La prise de conscience que nos propres capacités mentales n'est pas infinie, mais en fait, étonnamment limitée face à l'énorme complexité d'une base de code moderne, est importune et facilement supprimée ou rationalisée. Le fait que le code de test ne soit pas livré au client laisse penser facilement qu'il s'agit d'un citoyen de seconde classe et qu'il n'est pas essentiel par rapport au code commercial "essentiel". Et l’idée d’ajouter du code d’essai au code de l’entreprise semble doublement offensante pour beaucoup.

La difficulté à justifier cette pratique tient au fait que la vision globale de la création de valeur dans une entreprise logicielle n’est souvent comprise que par les supérieurs hiérarchiques de la société, mais que ces personnes n’ont pas la compréhension technique détaillée de le processus de codage nécessaire pour comprendre pourquoi les tests ne peuvent pas être supprimés. Par conséquent, ils sont trop souvent apaisés par des praticiens qui les assurent que les tests peuvent être une bonne idée en général, mais "nous sommes des programmeurs d'élite qui n'ont pas besoin de béquilles comme celle-ci" ou de "nous n'avons pas le temps pour ça", etc. Le fait que le succès d'une entreprise soit un jeu de chiffres et que l'on évite une dette technique, la qualité, etc. ne montre sa valeur qu'à long terme, ce qui signifie qu'ils sont souvent très sincères dans cette conviction.

En résumé: rendre le code vérifiable est un élément essentiel du processus de développement, il n’est pas différent de celui d’autres domaines (de nombreuses micropuces sont conçues avec une proportion substantielle d’éléments uniquement à des fins de test), mais il est très facile de faire abstraction des raisons cette. Ne tombez pas dans ce piège.

Kilian Foth
la source
39
Je dirais que cela dépend du type de changement. Il existe une différence entre rendre le code plus facile à tester et introduire des crochets spécifiques à un test qui ne devraient JAMAIS être utilisés en production. Personnellement, je me méfie de ce dernier, car Murphy ...
Matthieu M.
61
Les tests unitaires rompent souvent l’encapsulation et rendent le code à tester plus complexe qu’il ne serait autrement nécessaire (par exemple, en introduisant des types d’interface supplémentaires ou en ajoutant des indicateurs). Comme toujours en génie logiciel, chaque bonne pratique et chaque bonne règle a sa part de responsabilité. Produire aveuglément un grand nombre de tests unitaires peut avoir un effet néfaste sur la valeur commerciale, sans oublier que l'écriture et la maintenance des tests coûtent déjà du temps et des efforts. D'après mon expérience, les tests d'intégration ont un retour sur investissement beaucoup plus important et ont tendance à améliorer les architectures logicielles avec moins de compromis.
Christian Hackl
20
@Lee Bien sûr, mais vous devez vous demander si le recours à un type spécifique de tests justifie l'augmentation de la complexité du code. D'après mon expérience personnelle, les tests unitaires sont un excellent outil jusqu'au moment où ils nécessitent des modifications de conception fondamentales pour s'adapter aux moqueries. C'est là que je passe à un type de test différent. Écrire des tests unitaires au détriment de la complexification substantielle de l’architecture, dans le seul but de disposer de tests unitaires, relève du nombril.
Konrad Rudolph
21
@ChristianHackl pourquoi une unité de test casserait-elle l'encapsulation? J'ai constaté que pour le code sur lequel j'ai travaillé, s'il était perçu le besoin d' ajouter une fonctionnalité supplémentaire pour permettre les tests, le problème réel est que la fonction que vous devez tester doit être refactorisée, de sorte que toutes les fonctionnalités sont identiques. niveau d'abstraction (ce sont les différences de niveau d'abstraction qui créent généralement ce "besoin" de code supplémentaire), le code de niveau inférieur étant déplacé vers ses propres fonctions (testables).
Baldrickk
29
@ChristianHackl Les tests unitaires ne doivent jamais rompre l'encapsulation. Si vous essayez d'accéder à des variables privées, protégées ou locales à partir d'un test unitaire, vous le faites mal. Si vous testez la fonctionnalité foo, vous testez uniquement si cela a réellement fonctionné, et non pas si la variable locale x est la racine carrée de l'entrée y dans la troisième itération de la deuxième boucle. Si certaines fonctionnalités sont privées, qu’il en soit ainsi, vous les testerez de manière transitoire de toute façon. si c'est vraiment grand et privé? C'est un défaut de conception, mais probablement même pas possible en dehors de C et C ++ avec une séparation d'implémentation d'en-tête.
opa
75

Ce n'est pas aussi simple que vous pourriez le penser. Faisons le décomposer.

  • Écrire des tests unitaires est définitivement une bonne chose.

MAIS!

  • Toute modification de votre code peut introduire un bogue. Il est donc déconseillé de modifier le code sans motif commercial valable.

  • Votre webapi "très mince" ne semble pas être le meilleur cas pour les tests unitaires.

  • Changer le code et les tests en même temps est une mauvaise chose.

Je suggérerais l'approche suivante:

  1. Écrire des tests d'intégration . Cela ne devrait nécessiter aucune modification de code. Il vous donnera vos scénarios de test de base et vous permettra de vérifier que tout changement de code que vous apportez n'introduit aucun bogue.

  2. Assurez-vous que le nouveau code est testable et qu'il comporte des tests unitaires et d'intégration.

  3. Assurez-vous que votre chaîne de CI exécute des tests après les générations et les déploiements.

Une fois ces éléments configurés, commencez seulement à réfléchir à la refactorisation des projets hérités afin qu'ils puissent être testés.

Espérons que tout le monde aura tiré les leçons du processus et aura une bonne idée de l'endroit où les tests sont le plus nécessaires, de la manière dont vous souhaitez les structurer et de la valeur que cela apporte à l'entreprise.

EDIT : Depuis que j'ai écrit cette réponse, le PO a clarifié la question pour montrer qu'il parle de nouveau code, pas de modifications du code existant. J'ai peut-être naïvement pensé que le test est-il bon? l'argument a été réglé il y a quelques années.

Il est difficile d’imaginer les modifications de code requises par les tests unitaires, mais il ne s’agit en aucun cas d’une bonne pratique générale. Il serait probablement sage d’examiner les objections réelles, c’est peut-être le style de test unitaire auquel on s’oppose.

Ewan
la source
12
C'est une bien meilleure réponse que celle acceptée. Le déséquilibre des votes est consternant.
Konrad Rudolph
4
@Lee Un test unitaire doit tester une unité de fonctionnalité , qui peut correspondre ou non à une classe. Une unité de fonctionnalité doit être testée à son interface (qui peut être l'API dans ce cas). Les tests peuvent mettre en évidence les odeurs de conception et la nécessité d'appliquer une mise à niveau différente. Construisez vos systèmes à partir de petites pièces composables, elles seront plus faciles à raisonner et à tester.
Wes Toleman
2
@ KonradRudolph: Je suppose que j'ai mal compris le point où l'OP a ajouté que cette question concernait la conception d'un nouveau code et non la modification du code existant. Donc, il n'y a rien à rompre, ce qui rend la plupart de cette réponse non applicable.
Doc Brown le
1
Je ne suis absolument pas d'accord avec l'affirmation selon laquelle l'écriture de tests unitaires est toujours une bonne chose. Les tests unitaires ne sont bons que dans certains cas. Il est idiot d'utiliser des tests unitaires pour tester le code d'interface utilisateur (UI), ils sont conçus pour tester la logique métier. Aussi, il est bon d'écrire des tests unitaires pour remplacer les contrôles de compilation manquants (par exemple en Javascript). La plupart des codes frontaux doivent écrire exclusivement des tests de bout en bout, et non des tests unitaires.
Sulthan le
1
Les conceptions peuvent définitivement souffrir de "dommages induits par le test". Habituellement, la testabilité améliore la conception: lors de l’écriture de tests, vous remarquerez que quelque chose ne peut pas être récupéré mais doit être transmis, ce qui rend les interfaces plus claires, etc. Mais parfois, vous tomberez sur quelque chose qui nécessite une conception inconfortable uniquement pour les tests. Un exemple pourrait être un constructeur de test uniquement requis dans votre nouveau code en raison du code tiers existant qui utilise un singleton, par exemple. Lorsque cela se produit: prenez du recul et effectuez un test d'intégration uniquement, au lieu d'endommager votre propre conception au nom de la testabilité.
Anders Forsgren le
18

Concevoir du code pour qu'il soit intrinsèquement testable n'est pas une odeur de code; au contraire, c'est le signe d'un bon design. Il existe plusieurs modèles de conception bien connus et largement utilisés basés sur celui-ci (par exemple, Model-View-Presenter) qui offrent des tests faciles (plus faciles) comme un gros avantage.

Donc, si vous avez besoin d'écrire une interface pour votre classe concrète afin de la tester plus facilement, c'est une bonne chose. Si vous avez déjà la classe concrète, la plupart des IDE peuvent en extraire une interface, ce qui minimise les efforts requis. Garder les deux synchronisés demande un peu plus de travail, mais une interface ne devrait pas beaucoup changer de toute façon, et les avantages des tests risquent de compenser cet effort supplémentaire.

Par contre, comme @MatthieuM. Comme mentionné dans un commentaire, si vous ajoutez des points d’entrée spécifiques dans votre code qui ne doivent jamais être utilisés en production, uniquement à des fins de test, cela peut poser problème.

mmathis
la source
Ce problème peut être résolu par une analyse de code statique - indiquez les méthodes (par exemple, vous devez les nommer _ForTest) et vérifiez que la base de code contient des appels provenant de codes autres que les tests.
Riking
13

Il est très simple de comprendre que pour créer des tests unitaires, le code à tester doit avoir au moins certaines propriétés. Par exemple, si le code ne consiste pas en unités individuelles pouvant être testées isolément, le mot "test d'unité" n'a même pas de sens. Si le code n'a pas ces propriétés, il doit d'abord être modifié, c'est assez évident.

En théorie, on peut essayer d’écrire d’abord une unité de code testable, en appliquant tous les principes de SOLID, puis d’essayer d’écrire un test ultérieurement, sans modifier davantage le code original. Malheureusement, écrire du code qui est réellement testable par unité n’est pas toujours aussi simple, il est donc fort probable que certains changements seront nécessaires, qu’il ne détectera que lorsqu’on essaiera de créer les tests. C’est vrai pour le code même lorsque l’idée de tests unitaires a été conçue, et c’est encore plus vrai pour le code qui a été écrit pour lequel "la testabilité des unités" n’était pas à l’ordre du jour au début.

Il existe une approche bien connue qui tente de résoudre le problème en écrivant d’abord les tests unitaires - il s’appelle Test Driven Development (TDD), et elle peut certainement aider à rendre le code plus testable d’emblée, dès le départ.

Bien sûr, la réticence à changer de code par la suite pour le rendre testable se produit souvent dans une situation où le code a été testé manuellement et / ou fonctionne correctement en production, de sorte que le changer pourrait effectivement introduire de nouveaux bogues, c'est vrai. La meilleure solution consiste à créer d’abord une suite de tests de régression (qui peut souvent être mise en œuvre avec très peu de modifications de la base de code), ainsi que d’autres mesures connexes, telles que la révision du code ou de nouvelles sessions de tests manuels. Cela devrait vous donner suffisamment de confiance pour vous assurer que la refonte de certains composants internes ne casse rien d’important.

Doc Brown
la source
Intéressant que vous mentionniez TDD. Nous essayons d'introduire BDD / TDD, qui a également rencontré une certaine résistance - à savoir ce que "le code minimum à respecter" signifie vraiment.
Lee le
2
@Lee: apporter des changements dans une organisation provoque toujours une certaine résistance et il faut toujours un peu de temps pour adapter de nouvelles choses, ce n'est pas une sagesse nouvelle. Ceci est un problème de personnes.
Doc Brown le
Absolument. Je souhaite juste que nous avions eu plus de temps!
Lee
Il s'agit souvent de montrer aux gens que le faire de cette façon leur fera gagner du temps (et, espérons-le, rapidement aussi). Pourquoi faire quelque chose qui ne vous profitera pas?
Thorbjørn Ravn Andersen le
@ ThorbjørnRavnAndersen: L'équipe peut également montrer au PO que son approche permettra de gagner du temps. Qui sait? Mais je me demande si nous ne sommes pas réellement confrontés à des problèmes de nature moins technique ici; L'OP continue de venir ici pour nous dire ce que son équipe fait de mal (à son avis), comme s'il essayait de trouver des alliés pour sa cause. Il pourrait être plus utile de discuter effectivement le projet en même temps avec l'équipe, et non pas avec des étrangers sur Stack Exchange.
Christian Hackl
11

Je suis en désaccord avec l'affirmation (non fondée) que vous faites:

pour tester le service Web API, nous devrons nous moquer de cette usine

Ce n'est pas nécessairement vrai. Il existe de nombreuses façons d'écrire des tests, et il existe des façons d'écrire des tests unitaires sans implication. Plus important encore, il existe d'autres types de tests, tels que les tests fonctionnels ou d'intégration. Il est souvent possible de trouver une "ligne de test" au niveau d'une "interface" qui n'est pas un langage de programmation POO interface.

Quelques questions pour vous aider à trouver une couture de test alternative, qui pourrait être plus naturelle:

  • Vais-je un jour vouloir écrire une API Web mince sur une autre API?
  • Puis-je réduire la duplication de code entre l'API Web et l'API sous-jacente? Peut-on générer l'un en termes de l'autre?
  • Puis-je traiter l'intégralité de l'API Web et de l'API sous-jacente comme une seule "boîte noire" et faire des affirmations significatives sur le comportement de l'ensemble?
  • Si l'API Web devait être remplacée par une nouvelle implémentation à l'avenir, comment procéderions-nous?
  • Si l'API Web était remplacée par une nouvelle implémentation à l'avenir, les clients de l'API Web pourraient-ils le savoir? Si c'est le cas, comment?

Une autre affirmation non fondée que vous faites est à propos de DI:

soit nous concevons la classe de contrôleur API Web pour accepter DI (via son constructeur ou son séparateur), ce qui signifie que nous concevons une partie du contrôleur simplement pour permettre à DI et implémenter une interface dont nous n'avons pas autrement besoin, ou nous utilisons un tiers cadre comme Ninject pour ne pas avoir à concevoir le contrôleur de cette manière, mais il faudra quand même créer une interface.

L'injection de dépendance ne signifie pas nécessairement créer un nouveau interface. Par exemple, dans la cause d’un jeton d’authentification: pouvez-vous simplement créer un véritable jeton d’authentification par programme? Ensuite, le test peut créer de tels jetons et les injecter. Le processus de validation d'un jeton dépend-il d'un secret cryptographique quelconque? J'espère que vous n'avez pas codé en dur un secret - je m'attendrais à ce que vous puissiez le lire à partir du stockage, et dans ce cas, vous pouvez simplement utiliser un secret (bien connu) différent dans vos cas de test.

Cela ne veut pas dire que vous ne devriez jamais en créer un nouveau interface. Mais ne vous laissez pas décourager par le fait qu’il n’ya qu’une façon d’écrire un test ou de simuler un comportement. Si vous pensez en dehors de la boîte, vous pouvez généralement trouver une solution qui nécessitera un minimum de contorsions de votre code tout en vous donnant l'effet que vous souhaitez.

Daniel Pryden
la source
Remarques sur les assertions concernant les interfaces, mais même si nous ne les utilisions pas, il faudrait quand même injecter des objets, c’est la préoccupation du reste de l’équipe. c'est-à-dire que certains membres de l'équipe seraient satisfaits d'un ctr sans paramètre instanciant la mise en œuvre concrète et le laissant comme ça. En fait, un membre a émis l'idée d'utiliser la réflexion pour injecter des simulacres afin de ne pas avoir à concevoir de code pour les accepter. Ce qui est une odeur de code reeky imo
Lee
9

Vous avez de la chance car il s'agit d'un nouveau projet. J'ai constaté que Test Driven Design fonctionne très bien pour écrire du bon code (c'est pourquoi nous le faisons en premier lieu).

En déterminer à l' avance comment invoquer un morceau de code donné avec des données d'entrée réalistes, et d' obtenir des données de sortie réalistes que vous pouvez vérifier est comme prévu, vous faites la conception API très tôt dans le processus et ont une bonne chance d'obtenir un conception utile parce que vous n'êtes pas gêné par le code existant qui doit être réécrit pour tenir compte. En outre, il est plus facile à comprendre pour vos pairs, de sorte que vous puissiez avoir de bonnes discussions à nouveau au début du processus.

Notez que "utile" dans la phrase ci-dessus signifie non seulement que les méthodes résultantes sont faciles à invoquer, mais également que vous avez tendance à obtenir des interfaces propres, faciles à configurer dans les tests d'intégration et à rédiger des maquettes.

Considère-le. Surtout avec l'examen par les pairs. D'après mon expérience, l'investissement en temps et en efforts sera très vite rentabilisé.

Thorbjørn Ravn Andersen
la source
Nous avons également un problème avec le TDD, à savoir ce qui constitue le "code minimum à respecter". J'ai montré à l'équipe ce processus et ils se sont opposés à ce que nous n'écrivions pas simplement ce que nous avons déjà conçu - ce que je peux comprendre. Le "minimum" ne semble pas être défini. Si nous écrivons un test et avons des plans et des conceptions clairs, pourquoi ne pas l'écrire pour réussir le test?
Lee
@Lee "minimum code to pass" ... eh bien, cela peut sembler un peu stupide, mais c'est littéralement ce qu'il dit. Par exemple, si vous avez un test UserCanChangeTheirPassword, dans le test, vous appelez la fonction (pas encore existante) pour changer le mot de passe, puis vous affirmez que le mot de passe a bien été changé. Ensuite, vous écrivez la fonction, jusqu’à ce que vous puissiez exécuter le test et qu’il ne lève pas d’exception ni n’ait une assertion erronée. Si, à ce stade, vous avez une raison d'ajouter un code, cette raison est soumise à un autre test, par exemple UserCantChangePasswordToEmptyString.
R. Schmitz le
@Lee En fin de compte, vos tests constitueront la documentation de ce que fait votre code, à l'exception de la documentation qui vérifie si le code est rempli, au lieu d'être simplement de l'encre sur papier. Également comparer avec cette question - Une méthode CalculateFactorialqui retourne simplement 120 et le test réussit. C'est le minimum. Évidemment, ce n’est pas non plus ce qui était prévu, mais cela signifie simplement que vous avez besoin d’un autre test pour exprimer ce qui était prévu.
R. Schmitz le
1
@Lee Small Steps. Le minimum peut être plus que vous ne le pensez lorsque le code dépasse trivial. De plus, la conception que vous effectuez lors de la mise en œuvre intégrale peut être moins optimale car vous faites des suppositions sur la manière de procéder sans avoir écrit les tests qui le démontrent. Rappelez-vous à nouveau que le code devrait échouer au début.
Thorbjørn Ravn Andersen
1
En outre, les tests de régression sont très importants. Sont-ils à la portée de l'équipe?
Thorbjørn Ravn Andersen
8

Si vous devez modifier le code, c'est l'odeur du code.

D'après mon expérience personnelle, si mon code est difficile à écrire pour des tests, c'est un mauvais code. Ce n'est pas un mauvais code parce qu'il ne fonctionne pas ou ne fonctionne pas comme prévu, c'est mauvais parce que je ne peux pas comprendre rapidement pourquoi il fonctionne. Si je rencontre un bogue, je sais que le réparer sera long et pénible. Le code est également difficile / impossible à réutiliser.

Un bon code (propre) décompose les tâches en sections plus petites qui sont facilement compréhensibles en un coup d'œil (ou du moins en un bon aperçu). Tester ces petites sections est facile. Je peux également écrire des tests qui testent uniquement une partie de la base de code avec la même facilité si je suis assez confiant à propos des sous-sections (la réutilisation est également utile ici car elle a déjà été testée).

Conservez le code facile à tester, à refactoriser et à réutiliser dès le départ. Vous ne vous ferez pas tuer à chaque fois que vous devrez apporter des modifications.

Je tape ceci en reconstruisant complètement un projet qui aurait dû être un prototype jetable en code plus propre. Il est bien préférable de commencer dès le début et de reformuler le plus tôt possible le mauvais code plutôt que de regarder un écran pendant des heures sans avoir peur de toucher à quoi que ce soit de peur de casser quelque chose qui ne fonctionne que partiellement.

David
la source
3
"Prototype Throwaway" - chaque projet commence sa vie comme l'un de ceux-là ... il est préférable de penser à des choses qui ne le sont jamais. en tapant ceci comme je suis .. devinez quoi? ... refactoring d'un prototype jetable qui s'est avéré ne pas être;)
Algy Taylor
4
Si vous voulez être sûr qu'un prototype à jeter sera jeté, écrivez-le dans un langage de prototype qui ne sera jamais autorisé en production. Clojure et Python sont de bons choix.
Thorbjørn Ravn Andersen
2
@ ThorbjørnRavnAndersen Cela m'a fait rire. Était-ce censé être un creuset pour ces langues? :)
Lee
@ Lee. Non, juste des exemples de langages qui pourraient ne pas être acceptables pour la production - généralement parce que personne dans l'organisation ne peut les maintenir car ils ne les connaissent pas et que leurs courbes d'apprentissage sont raides. Si ceux-ci sont acceptables, choisissez-en un autre.
Thorbjørn Ravn Andersen
4

Je dirais que l'écriture de code qui ne peut pas être testé à l'unité est une odeur de code. En général, si votre code ne peut pas être testé à l'unité, il n'est pas modulaire, ce qui le rend difficile à comprendre, à maintenir ou à améliorer. Peut-être que si le code est un code collé qui n'a de sens que pour les tests d'intégration, vous pouvez substituer les tests d'intégration aux tests unitaires, mais même si l'intégration échoue, vous devrez isoler le problème et les tests unitaires sont un excellent moyen de fais le.

Vous dites

Nous prévoyons de créer une fabrique qui renverra un type de méthode d'authentification. Nous n'avons pas besoin qu'il hérite d'une interface car nous ne prévoyons pas qu'il s'agira jamais d'un type autre que concret. Cependant, pour tester un peu le service API Web, nous devrons nous moquer de cette usine.

Je ne suis pas vraiment ça. La raison pour laquelle une fabrique crée quelque chose est pour vous permettre de changer de fabrique ou de changer facilement ce que l’usine crée, de sorte que les autres parties du code n’ont pas besoin de changer. Si votre méthode d'authentification ne changera jamais, alors l'usine est un code inutile. Toutefois, si vous souhaitez utiliser une méthode d’authentification différente de celle utilisée en production, il est judicieux de disposer d’une usine qui renvoie une méthode d’authentification différente de celle utilisée en production.

Vous n'avez pas besoin de DI ou de Mocks pour cela. Vous avez juste besoin que votre usine prenne en charge les différents types d'authentification et soit configurable, par exemple à partir d'un fichier de configuration ou d'une variable d'environnement.

Vieux pro
la source
2

Dans chaque discipline d'ingénierie à laquelle je peux penser, il n'y a qu'un seul moyen d'atteindre des niveaux de qualité décents ou supérieurs:

Pour tenir compte des inspections / tests dans la conception.

Cela est vrai dans la construction, la conception de puces, le développement de logiciels et la fabrication. Cela ne signifie pas pour autant que les tests constituent le pilier sur lequel chaque conception doit être construite, pas du tout. Mais avec chaque décision de conception, les concepteurs doivent être clairs sur les impacts sur les coûts de test et prendre une décision consciente quant au compromis.

Dans certains cas, les tests manuels ou automatisés (par exemple, le sélénium) seront plus pratiques que les tests unitaires, tout en fournissant également une couverture de test acceptable. Dans de rares cas, jeter quelque chose qui n’est presque pas testé peut également être acceptable. Mais ces décisions doivent être prises au cas par cas. L'appel d'une conception qui permet de tester une "odeur de code" indique un sérieux manque d'expérience.

Peter
la source
1

J'ai constaté que les tests unitaires (et d'autres types de tests automatisés) ont tendance à réduire les odeurs de code, et je ne peux pas penser à un seul exemple où ils introduisent des odeurs de code. Les tests unitaires vous obligent généralement à écrire un meilleur code. Si vous ne pouvez pas utiliser une méthode facilement sous test, pourquoi cela devrait-il être plus facile dans votre code?

Des tests unitaires bien écrits vous montrent comment le code est destiné à être utilisé. Ils sont une forme de documentation exécutable. J'ai vu des tests unitaires trop longs et hideusement écrits qui ne pouvaient tout simplement pas être compris. N'écris pas ça! Si vous devez rédiger de longs tests pour configurer vos cours, ceux-ci doivent être refactorisés.

Les tests unitaires mettront en évidence l'emplacement de certaines odeurs de votre code. Je conseillerais de lire Michael C. Feathers, Travailler efficacement avec Legacy Code . Même si votre projet est nouveau, s'il n'a pas déjà (ou plusieurs) tests unitaires, vous aurez peut-être besoin de techniques peu évidentes pour que votre code soit correctement testé.

CJ Dennis
la source
3
Vous pouvez être tenté d'introduire de nombreuses couches indirection afin de pouvoir tester, puis ne les utilisez jamais comme prévu.
Thorbjørn Ravn Andersen
1

En un mot:

Le code testable est (généralement) un code maintenable - ou plutôt, un code difficile à tester est généralement difficile à maintenir. Concevoir un code qui ne soit pas testable revient à concevoir une machine qui ne peut pas être réparée - prenez en pitié le pauvre client qui sera chargé de le réparer (cela pourrait être vous-même).

Un exemple est que nous prévoyons de créer une usine qui renverra un type de méthode d'authentification. Nous n'avons pas besoin qu'il hérite d'une interface car nous ne prévoyons pas qu'il s'agira jamais d'un type autre que concret.

Vous savez que vous aurez besoin de cinq types différents de méthodes d'authentification dans trois ans, maintenant que vous l'avez dit, n'est-ce pas? Les exigences changent et, bien que vous évitiez de trop modifier votre conception, une conception testable signifie que votre conception a assez de raccords pour être modifiée sans (trop) de douleur - et que les tests de module vous fourniront un moyen automatisé de vérifier que vos changements ne cassent rien.

CharonX
la source
1

Concevoir autour de l'injection de dépendance n'est pas une odeur de code, c'est une bonne pratique. L'utilisation de DI ne se limite pas à la testabilité. Construire vos composants autour des aides au DI aide à la modularité et à la réutilisabilité, permet plus facilement l'échange de composants majeurs (comme une couche d'interface de base de données). Bien que cela ajoute un certain degré de complexité, il permet une meilleure séparation des couches et des fonctionnalités, ce qui facilite la gestion et la navigation de la complexité. Cela facilite la validation du comportement de chaque composant, ce qui réduit le nombre de bogues et facilite également le suivi des bogues.

Zenilogix
la source
1
"bien fait" est un problème. Je dois maintenir deux projets où DI a été mal fait (bien que le but soit de le faire "bien"). Cela rend le code pur et horrible et bien pire que les projets hérités sans DI et tests unitaires. Obtenir une DI correcte n'est pas facile.
Jan
@ Jan c'est intéressant. Comment l'ont-ils mal fait?
Lee
1
Le projet @Lee One est un service qui a besoin d'un temps de démarrage rapide mais qui est terriblement lent au démarrage, car toute l'initialisation de la classe est effectuée à l'avance par le cadre DI (Castle Windsor en C #). Un autre problème que je vois dans ces projets est de mélanger DI avec la création d’objets avec "nouveau", en contournant le DI. Cela rend les essais difficiles à nouveau et conduit à de mauvaises conditions de course.
Jan
1

Cela signifie essentiellement que nous concevons la classe de contrôleur API Web pour accepter DI (via son constructeur ou son séparateur), ce qui signifie que nous concevons une partie du contrôleur simplement pour permettre à DI et implémenter une interface dont nous n'avons pas autrement besoin, ou nous utilisons Un framework tiers, comme Ninject, évite de devoir concevoir le contrôleur de cette manière, mais il reste à créer une interface.

Regardons la différence entre un testable:

public class MyController : Controller
{
    private readonly IMyDependency _thing;

    public MyController(IMyDependency thing)
    {
        _thing = thing;
    }
}

et contrôleur non testable:

public class MyController : Controller
{
}

La première option comporte littéralement 5 lignes de code supplémentaires, dont deux peuvent être générées automatiquement par Visual Studio. Une fois que vous avez configuré votre infrastructure d'injection de dépendance pour substituer un type concret IMyDependencylors de l'exécution, ce qui correspond à une autre ligne de code pour tout environnement de DI correct. .

6 lignes de code supplémentaires pour permettre la testabilité ... et vos collègues soutiennent que c'est "trop ​​de travail"? Cet argument ne va pas avec moi, et il ne devrait pas voler avec vous.

Et vous n'avez pas besoin de créer et d'implémenter une interface de test: Moq , par exemple, vous permet de simuler le comportement d'un type concret à des fins de test unitaire. Bien sûr, cela ne vous sera pas très utile si vous ne pouvez pas injecter ces types dans les classes que vous testez.

L'injection de dépendance est l'une de ces choses qu'une fois que vous la comprenez, vous vous demandez "comment ai-je travaillé sans cela?". C'est simple, efficace et ça donne du sens. S'il vous plaît, ne laissez pas le manque de compréhension de vos collègues sur de nouvelles choses empêcher de rendre votre projet testable.

Ian Kemp
la source
1
Ce que vous êtes si prompt à rejeter comme "manque de compréhension de nouvelles choses" peut s'avérer être une bonne compréhension de vieilles choses. L'injection de dépendance n'est certainement pas nouvelle. L'idée, et probablement les premières implémentations, datent de plusieurs décennies. Et oui, je pense que votre réponse est un exemple de code devenant plus compliqué à cause des tests unitaires, et éventuellement un exemple de tests unitaires ne respectant pas l'encapsulation (car qui dit que la classe a un constructeur public?). J'ai souvent supprimé l'injection de dépendance des bases de code que j'avais héritées de quelqu'un d'autre, à cause des compromis.
Christian Hackl
Les contrôleurs ont toujours un constructeur public, implicite ou non, car MVC le requiert. "Compliqué" - peut-être si vous ne comprenez pas le fonctionnement des constructeurs. Encapsulation - oui dans certains cas, mais le débat DI vs encapsulation est un sujet très subjectif qui n’aidera pas. En particulier pour la plupart des applications, DI vous servira mieux que l’OMI encapsulation.
Ian Kemp
En ce qui concerne les constructeurs publics: en effet, il s’agit d’une particularité du cadre utilisé. Je pensais au cas plus général d'une classe ordinaire qui n'est pas instanciée par un cadre. Pourquoi pensez-vous que l'affichage de paramètres de méthode supplémentaires en tant que complexité ajoutée équivaut à un manque de compréhension du fonctionnement des constructeurs? Cependant, j'apprécie que vous reconnaissiez l'existence d'un compromis entre ID et encapsulation.
Christian Hackl
0

Quand j'écris des tests unitaires, je commence à penser à ce qui pourrait mal tourner dans mon code. Cela m'aide à améliorer la conception du code et à appliquer le principe de responsabilité unique (SRP). De plus, lorsque je reviens modifier le même code quelques mois plus tard, cela m'aide à confirmer que les fonctionnalités existantes ne sont pas endommagées.

Il y a une tendance à utiliser autant que possible des fonctions pures (applications sans serveur). Les tests unitaires m'aident à isoler l'état et à écrire des fonctions pures.

Plus précisément, nous aurons un service API Web qui sera très mince. Sa principale responsabilité consistera à organiser les requêtes / réponses Web et à appeler une API sous-jacente contenant la logique métier.

Commencez par écrire les tests unitaires pour l'API sous-jacente et si vous avez suffisamment de temps de développement, vous devez également rédiger des tests pour le service API Web mince.

Les tests unitaires TL; DR contribuent à améliorer la qualité du code et à apporter de futures modifications au code sans risque. Cela améliore également la lisibilité du code. Utilisez des tests au lieu de commentaires pour faire valoir votre point de vue.

Ashutosh
la source
0

En bout de ligne, et quel que devrait être votre argument avec le lot réticent, c'est qu'il n'y a pas de conflit. La grosse erreur semble avoir été que quelqu'un a inventé l'idée de "concevoir pour tester" des personnes qui détestent les tests. Ils auraient dû simplement fermer la bouche ou le dire différemment, par exemple: "prenons le temps de bien faire les choses".

L'idée selon laquelle "vous devez implémenter une interface" pour rendre quelque chose testable est fausse. L'interface est déjà implémentée, elle n'est simplement pas encore déclarée dans la déclaration de classe. Il s'agit de reconnaître les méthodes publiques existantes, de copier leurs signatures dans une interface et de déclarer cette interface dans la déclaration de la classe. Aucune programmation, aucune modification de la logique existante.

Apparemment, certaines personnes ont une idée différente à ce sujet. Je vous suggère d'essayer de résoudre ce problème en premier.

Martin Maat
la source