Notre collègue fait la promotion de la rédaction de tests unitaires comme nous aidant à affiner notre conception et à refactoriser les choses, mais je ne vois pas comment. Si je charge un fichier CSV et que je l'analyse, comment le test unitaire (validation des valeurs dans les champs) va-t-il m'aider à vérifier ma conception? Il a mentionné le couplage et la modularité, etc. mais pour moi cela n'a pas beaucoup de sens - mais je n'ai pas beaucoup de fond théorique, cependant.
Ce n'est pas la même chose que la question que vous avez marquée en double, je serais intéressé par des exemples concrets en quoi cela aide, pas seulement la théorie disant "ça aide". J'aime la réponse ci-dessous et le commentaire mais j'aimerais en savoir plus.
Non seulement les tests unitaires facilitent la conception, mais c'est l'un de leurs principaux avantages.
Lorsque vous écrivez d'abord votre code de test, vous constaterez que toutes les "conditions" d'une unité de code donnée sont naturellement repoussées vers les dépendances (généralement via des mocks ou des stubs) lorsque vous les assumez dans votre code.
"Étant donné la condition x, attendez-vous au comportement y", deviendra souvent un stub pour fournir x
(qui est un scénario dans lequel le test doit vérifier le comportement du composant actuel) et y
deviendra une maquette, un appel à qui sera vérifié à la fin du test (sauf s'il s'agit d'un "devrait renvoyer y
," dans ce cas, le test vérifiera simplement la valeur de retour explicitement).
Ensuite, une fois que cette unité se comporte comme spécifié, vous passez à l'écriture des dépendances (pour x
et y
) que vous avez découvertes.
Cela rend l'écriture d'un code propre et modulaire un processus très facile et naturel, où sinon il est souvent facile de brouiller les responsabilités et de coupler les comportements sans s'en rendre compte.
Lorsque l'écriture de tests pour un morceau de code devient difficile parce qu'il y a trop de choses à coller ou à simuler, ou parce que les choses sont trop étroitement liées, vous savez que vous avez des améliorations à apporter à votre code.
Lorsque "changer les tests" devient un fardeau parce qu'il y a tellement de comportements dans une seule unité, vous savez que vous avez des améliorations à apporter dans votre code (ou simplement dans votre approche de l'écriture de tests - mais ce n'est généralement pas le cas dans mon expérience) .
Lorsque vos scénarios deviennent trop compliqués ("si x
et y
et z
alors ...") parce que vous avez besoin d'abstraire davantage, vous savez que vous avez des améliorations à apporter dans votre code.
Lorsque vous vous retrouvez avec les mêmes tests dans deux appareils différents en raison de la duplication et de la redondance, vous savez que vous avez des améliorations à apporter à votre code.
Voici un excellent discours de Michael Feathers démontrant la relation très étroite entre la testabilité et la conception dans le code (initialement publié par displayName dans les commentaires). L'exposé aborde également certaines plaintes courantes et idées fausses sur la bonne conception et la testabilité en général.
La grande chose au sujet des tests unitaires est qu'ils vous permettent d'utiliser votre code comme les autres programmeurs utiliseront votre code.
Si votre code est gênant pour le test unitaire, il sera probablement difficile à utiliser. Si vous ne pouvez pas injecter de dépendances sans sauter à travers des cerceaux, alors votre code sera probablement rigide à utiliser. Et si vous avez besoin de passer beaucoup de temps à configurer des données ou à déterminer dans quel ordre faire les choses, votre code sous test a probablement trop de couplage et sera difficile à travailler.
Cela m'a pris un certain temps à réaliser, mais le véritable avantage (pour moi, votre kilométrage peut varier) de faire du développement piloté par les tests (en utilisant tests unitaires) est que vous devez faire la conception de l'API à l'avance!
Une approche typique du développement consiste à comprendre d'abord comment résoudre un problème donné, et avec cette connaissance et la conception de la mise en œuvre initiale, un moyen d'invoquer votre solution. Cela peut donner des résultats assez intéressants.
Lorsque vous faites TDD, vous devez tout d'abord écrire le code qui tilisera votre solution. Paramètres d'entrée et sortie attendue pour vous assurer qu'il est correct. À votre tour, cela vous oblige à comprendre ce que vous devez réellement faire pour que vous puissiez créer des tests significatifs. Alors et seulement alors, vous implémentez la solution. D'après mon expérience, lorsque vous savez exactement ce que votre code est censé accomplir, cela devient plus clair.
Ensuite, après l'implémentation, les tests unitaires vous aident à vous assurer que le refactoring ne rompt pas les fonctionnalités et fournissent de la documentation sur la façon d'utiliser votre code (qui, selon vous, est juste après le test!). Mais ceux-ci sont secondaires - le plus grand avantage est l'état d'esprit dans la création du code en premier lieu.
Je conviens à 100% que les tests unitaires aident "à nous aider à affiner notre conception et à refactoriser les choses".
Je suis de deux avis pour savoir s'ils vous aident à faire conception initiale. Oui, ils révèlent des défauts évidents et vous obligent à réfléchir à "comment rendre le code testable"? Cela devrait entraîner moins d'effets secondaires, une configuration et des installations plus faciles, etc.
Cependant, d'après mon expérience, des tests unitaires trop simplistes, écrits avant que vous compreniez vraiment ce que devrait être la conception (certes, c'est une exagération de TDD dur, mais trop souvent les codeurs écrivent un test avant de penser beaucoup) conduisent souvent à l'anémie modèles de domaine qui exposent trop de internes.
Mon expérience avec TDD remonte à plusieurs années, je suis donc intéressé à savoir quelles nouvelles techniques pourraient aider à écrire des tests qui ne biaisent pas trop la conception sous-jacente. Merci.
Le test unitaire vous permet de voir comment fonctionnent les interfaces entre les fonctions et vous donne souvent un aperçu de la façon d'améliorer à la fois la conception locale et la conception globale. De plus, si vous développez vos tests unitaires tout en développant votre code, vous disposez d'une suite de tests de régression prête à l'emploi. Peu importe que vous développiez une interface utilisateur ou une bibliothèque d'arrière-plan.
Une fois le programme développé (avec tests unitaires), au fur et à mesure que les bogues sont découverts, vous pouvez ajouter des tests pour confirmer que les bogues sont corrigés.
J'utilise TDD pour certains de mes projets. Je mets beaucoup d'efforts à créer des exemples que je tire de manuels ou d'articles qui sont considérés comme corrects, et teste le code que je développe en utilisant ces exemples. Tout malentendu que j'ai concernant les méthodes devient très apparent.
J'ai tendance à être un peu plus lâche que certains de mes collègues, car je me fiche que le code soit écrit en premier ou que le test soit écrit en premier.
Lorsque vous souhaitez tester un par un votre analyseur détectant correctement la délimitation de valeur, vous pouvez lui passer une ligne à partir d'un fichier CSV. Pour rendre votre test direct et court, vous voudrez peut-être le tester via une méthode qui accepte une ligne.
Cela vous fera automatiquement séparer la lecture des lignes de la lecture des valeurs individuelles.
À un autre niveau, vous ne voudrez peut-être pas mettre toutes sortes de fichiers CSV physiques dans votre projet de test, mais faites quelque chose de plus lisible, déclarant simplement une grande chaîne CSV dans votre test pour améliorer la lisibilité et l'objectif du test. Cela vous amènera à découpler votre analyseur de toute E/S que vous feriez ailleurs.
Juste un exemple de base, commencez simplement à le pratiquer, vous ressentirez la magie à un moment donné (je l'ai).
En termes simples, l'écriture de tests unitaires permet d'exposer les failles de votre code.
Ce spectaculaire guide pour écrire du code testable , écrit par Jonathan Wolter, Russ Ruffer et Miško Hevery, contient de nombreux exemples de la façon dont les défauts de code, qui empêchent les tests, empêchent également la réutilisation facile et la flexibilité du même code. Ainsi, si votre code est testable, il est plus facile à utiliser. La plupart des "morales" sont des astuces ridiculement simples qui améliorent considérablement la conception du code ( injection de dépendance FTW).
Par exemple: Il est très difficile de tester si la méthode computeStuff fonctionne correctement lorsque le cache commence à expulser des éléments. C'est parce que vous devez ajouter manuellement de la merde au cache jusqu'à ce que le "bigCache" soit presque plein.
public OopsIHardcoded {
Cache cacheOfExpensiveComputations;
OopsIHardcoded() {
this.cacheOfExpensiveComputation = buildBigCache();
}
ExpensiveValue computeStuff() {
//DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
}
}
Cependant, lorsque nous utilisons l'injection de dépendances, il est beaucoup plus facile de tester si la méthode computeStuff fonctionne correctement lorsque le cache commence à expulser des éléments. Tout ce que nous faisons est de créer un test dans lequel nous appelons new HereIUseDI(buildSmallCache());
Remarquez, nous avons un contrôle plus nuancé de l'objet et il paie immédiatement des dividendes.
public HereIUseDI {
Cache cacheOfExpensiveComputations;
HereIUseDI(Cache cache) {
this.cacheOfExpensiveComputation = cache;
}
ExpensiveValue computeStuff() {
//DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
}
}
Des avantages similaires peuvent être obtenus lorsque notre code nécessite des données qui sont généralement conservées dans une base de données ... il suffit de transmettre EXACTEMENT les données dont vous avez besoin.
Selon ce que l'on entend par "tests unitaires", je ne pense pas que les tests unitaires de bas niveau facilitent une bonne conception autant qu'un niveau légèrement supérieur tests d'intégration - tests qui testent qu'un groupe d'acteurs (classes, fonctions, etc.) dans votre code se combinent correctement pour produire tout un tas de comportements souhaitables qui ont convenu entre l'équipe de développement et le propriétaire du produit.
Si vous pouvez écrire des tests à ces niveaux, cela vous pousse à créer du code Nice, logique, semblable à une API qui ne nécessite pas beaucoup de dépendances folles - le désir d'avoir une configuration de test simple vous conduira naturellement à ne pas avoir beaucoup de dépendances folles ou code étroitement couplé.
Ne vous y trompez pas - Les tests unitaires peuvent vous conduire à une mauvaise conception, ainsi qu'à une bonne conception. J'ai vu des développeurs prendre un peu de code qui a déjà une conception logique agréable et une seule préoccupation, le séparer et introduire plus d'interfaces uniquement à des fins de test, et en conséquence rendre le code moins lisible et plus difficile à modifier , et peut-être même avoir plus de bogues si le développeur a décidé que de nombreux tests unitaires de bas niveau ne les obligent pas à avoir des tests de niveau supérieur. Un exemple préféré en particulier est un bug que j'ai corrigé où il y avait beaucoup de code très testable et décomposé concernant l'obtention d'informations sur et hors du presse-papiers. Tous décomposés et découplés à de très petits niveaux de détail, avec beaucoup d'interfaces, beaucoup de simulacres dans les tests et d'autres trucs amusants. Un seul problème - il n'y avait pas de code qui ait réellement interagi avec le mécanisme du presse-papiers du système d'exploitation, donc le code en production n'a rien fait.
Les tests unitaires peuvent certainement orienter votre conception - mais ils ne vous guident pas automatiquement vers une bonne conception. Vous avez besoin d'avoir des idées sur ce qu'est une bonne conception qui va au-delà de 'ce code est testé, donc c'est testable, donc c'est bon ".
Bien sûr, si vous êtes une de ces personnes pour qui "tests unitaires" signifie "tous les tests automatisés qui ne sont pas effectués via l'interface utilisateur", certains de ces avertissements pourraient ne pas être aussi pertinents - comme je l'ai dit, je pense que ceux qui sont plus élevés Les tests d'intégration de niveau supérieur sont souvent les plus utiles pour piloter votre conception.