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.
Réponses:
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.
la source
Ce n'est pas aussi simple que vous pourriez le penser. Faisons le décomposer.
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:
É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.
Assurez-vous que le nouveau code est testable et qu'il comporte des tests unitaires et d'intégration.
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.
la source
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.
la source
_ForTest
) et vérifiez que la base de code contient des appels provenant de codes autres que les tests.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.
la source
Je suis en désaccord avec l'affirmation (non fondée) que vous faites:
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:
Une autre affirmation non fondée que vous faites est à propos de DI:
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.la source
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é.
la source
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 exempleUserCantChangePasswordToEmptyString
.CalculateFactorial
qui 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.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.
la source
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
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.
la source
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.
la source
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é.
la source
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).
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.
la source
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.
la source
Regardons la différence entre un testable:
et contrôleur non testable:
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
IMyDependency
lors 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.
la source
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.
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.
la source
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.
la source