Comment les tests unitaires facilitent-ils la conception?

43

Notre collègue estime que la rédaction de tests unitaires nous aide réellement à affiner notre travail de conception et de refactorisation, mais je ne vois pas comment. Si je charge et analyse un fichier CSV, comment le test unitaire (validation des valeurs dans les champs) va-t-il m'aider à vérifier ma conception? Il a parlé de couplage et de modularité, etc., mais pour moi cela n’a pas beaucoup de sens - mais je n’ai pas beaucoup de connaissances théoriques, cependant.

Ce n'est pas la même chose que la question que vous avez marquée comme étant en double, je serais intéressé par des exemples concrets de la façon dont cela aide, et pas seulement par la théorie disant "ça aide". J'aime la réponse ci-dessous et le commentaire, mais j'aimerais en savoir plus.

Utilisateur039402
la source
7
Duplication possible de TDD mène-t
Gnat
3
La réponse ci-dessous est vraiment tout ce que vous devez savoir. Un gars qui écrit tranquillement du code simple contre des tests unitaires qui fonctionnent correctement, est facile à vérifier et est déjà documenté.
Robert Harvey
4
@gnat faire des tests unitaires n'implique pas automatiquement TDD, c'est une question différente
Joppe
11
"test unitaire (validation des valeurs dans les champs)" - vous semblez confondre les tests unitaires avec la validation d'entrée.
jonrsharpe
1
@jonrsharpe Etant donné que le code analyse un fichier CSV, il parle peut-être d'un test unitaire réel qui vérifie qu'une certaine chaîne CSV donne la sortie attendue.
JollyJoker

Réponses:

3

Non seulement les tests unitaires facilitent la conception, mais c’est l’un de leurs principaux avantages.

L'écriture test-first élimine la modularité et la structure de code propre.

Lorsque vous écrivez votre test de code en premier, vous constaterez que toutes les "conditions" d'une unité de code donnée sont naturellement poussées vers des dépendances (généralement via des imitations ou des stubs) lorsque vous les supposez dans votre code.

"Étant donné la condition x, attendez le comportement y", deviendra souvent une souche à fournir x(ce qui est un scénario dans lequel le test doit vérifier le comportement du composant actuel) et ydeviendra une simulation, un appel qui sera vérifié à la fin du test (à moins qu'il ne s'agisse d'un "devrait retourner y", auquel cas le test vérifiera simplement la valeur de retour de manière explicite).

Ensuite, une fois que cette unité se comporte comme spécifié, vous écrivez les dépendances (pour xet y) que vous avez découvertes.

L'écriture de code propre et modulaire est donc un processus très simple et naturel. Sinon, il est souvent facile de brouiller responsabilités et comportements de couple sans s'en rendre compte.

Écrire des tests plus tard vous dira quand votre code est mal structuré.

Lorsque rédiger des tests pour un élément de code devient difficile parce qu'il y a trop d'éléments à bloquer ou à imiter, ou parce que les éléments sont trop étroitement liés, vous savez que vous devez apporter des améliorations à votre code.

Lorsque "changer de test" devient un fardeau, car il y a tellement de comportements dans une même unité, vous savez que vous devez apporter des améliorations à votre code (ou simplement à votre approche de la rédaction de tests - mais ce n'est généralement pas le cas dans mon expérience). .

Lorsque vos scénarios deviennent trop compliquées ( « si xet yet zpuis ... ») parce que vous avez besoin de plus abstrait, 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 devez apporter des améliorations à votre code.

Voici une excellente conférence de Michael Feathers démontrant la relation très étroite qui existe entre testabilité et conception en code (publié à l'origine par displayName dans les commentaires). La conférence aborde également certaines des plaintes communes et des idées fausses sur la bonne conception et la testabilité en général.

Ant P
la source
@SSECommunity: Avec seulement 2 votes positifs à la date d'aujourd'hui, cette réponse est très facile à oublier. Je recommande vivement le Talk de Michael Feathers qui a été associé à cette réponse.
displayName
103

L'avantage des tests unitaires est qu'ils vous permettent d'utiliser votre code de la même manière que les autres programmeurs.

Si votre code est difficile à tester à l’unité, il sera probablement difficile à utiliser. Si vous ne pouvez pas injecter des dépendances sans passer par des cerceaux, votre code sera probablement inflexible. Et si vous avez besoin de passer beaucoup de temps à configurer des données ou à déterminer l'ordre dans lequel vous souhaitez effectuer certaines tâches, votre code sous test comporte probablement trop de couplages et il sera difficile de travailler avec vous.

Telastyn
la source
7
Très bonne réponse. J'aime toujours penser à mes tests en tant que premier client du code; s'il est pénible d'écrire les tests, ce sera pénible d'écrire du code qui consomme l'API ou quoi que je développe.
Stephen Byrne
41
D'après mon expérience, la plupart des tests unitaires n'utilisent pas votre code de la même manière que les autres programmeurs utiliseront votre code. Ils utilisent votre code comme les tests unitaires utiliseront le code. Certes, ils vont révéler de nombreuses lacunes graves. Toutefois, une API conçue pour le test unitaire peut ne pas être celle qui convient le mieux à une utilisation générale. Les tests unitaires écrits de manière simpliste nécessitent souvent que le code sous-jacent expose trop d'internaux. Encore une fois, sur la base de mon expérience, j'aimerais savoir comment vous avez géré cela. (Voir ma réponse ci-dessous)
user949300
7
@ user949300 - Je ne suis pas un grand partisan du test. Ma réponse est basée sur l'idée de code (et certainement de conception) en premier. Les API ne doivent pas être conçues pour les tests unitaires, elles doivent être conçues pour votre client. Les tests unitaires aident à approcher votre client, mais ce sont des outils. Ils sont là pour vous servir, pas l'inverse. Et ils ne vont certainement pas vous empêcher de créer du code pourri.
Telastyn
3
D'après mon expérience, le plus gros problème avec les tests unitaires est qu'écrire de bons tests est aussi difficile que d'écrire de bons codes. Si vous ne pouvez pas distinguer un bon code d’un mauvais code, écrire des tests unitaires n’améliorera pas votre code. Lors de la rédaction du test unitaire, vous devez être en mesure de faire la distinction entre une utilisation douce et agréable et une utilisation "maladroite" ou difficile. Ils peuvent vous faire utiliser un peu votre code, mais ils ne vous obligent pas à reconnaître que ce que vous faites est mauvais.
jpmc26
2
@ user949300 - l'exemple classique que j'avais en tête est un référentiel qui a besoin d'une chaîne de connexion. Supposons que vous exposiez cela en tant que propriété accessible en écriture publique et que vous deviez la définir après new () un référentiel. L'idée est qu'après 5 ou 6 fois, vous ayez écrit un test qui oublie de franchir cette étape - et vous bloque ainsi - vous serez naturellement enclin à forcer connString à devenir un invariant de classe - passé dans le constructeur - ce qui rendra votre API améliorée et rendant plus probable l' écriture d'un code de production évitant ce piège. Ce n'est pas une garantie, mais ça aide, imo.
Stephen Byrne
31

J'ai mis beaucoup de temps à comprendre, mais le véritable avantage (pour moi, votre kilométrage peut varier) du développement piloté par les tests (à l' aide de tests unitaires) est que vous devez concevoir l'API à l'avance !

Une approche typique du développement consiste d'abord à trouver le moyen de résoudre un problème donné et, avec cette connaissance et cette conception de mise en œuvre initiale, à invoquer votre solution. Cela peut donner des résultats plutôt intéressants.

Lorsque vous utilisez TDD, vous devez au tout premier écrire le code qui utilisera votre solution. Les paramètres d'entrée et la sortie attendue vous permettent de vous assurer que tout est correct. Cela vous oblige à déterminer ce dont vous avez réellement besoin pour pouvoir créer des tests significatifs. Alors et seulement alors, vous implémentez la solution. J’ai également constaté que lorsque vous savez exactement ce que votre code est censé réaliser, cela devient plus clair.

Ensuite, après la mise en œuvre, les tests unitaires vous aident à vous assurer que le refactoring ne rompt pas avec les fonctionnalités, et vous expliquent comment utiliser votre code (ce que vous savez être exact si le test a réussi!). Mais ceux-ci sont secondaires - le plus grand avantage est la mentalité dans la création du code.

Thorbjørn Ravn Andersen
la source
C’est certes un avantage, mais je ne pense pas que ce soit le «véritable» avantage. L’avantage réel vient du fait que les tests de code poussent naturellement les «conditions» vers les dépendances et annulent la surinjection de dépendances ) avant de commencer.
Ant P
Le problème est que vous écrivez tout un ensemble de tests qui correspondent à cette API. Cela ne fonctionne pas exactement comme vous le souhaitez et vous devez réécrire votre code et tous les tests. Pour les API destinées au public, il est probable qu'elles ne changeront pas et cette approche convient. Cependant, les API pour le code utilisé uniquement en interne changent beaucoup lorsque vous apprenez comment implémenter une fonctionnalité qui requiert la collaboration de nombreuses API semi-privées
Juan Mendes
@AntP Oui, cela fait partie de la conception de l'API.
Thorbjørn Ravn Andersen
@JuanMendes Ce n'est pas rare et ces tests devront être modifiés, comme tout autre code lorsque vous modifiez les exigences. Un bon IDE vous aidera à refactoriser les classes dans le cadre du travail effectué automatiquement lorsque vous modifiez les signatures de méthode, etc.
Thorbjørn Ravn Andersen
@JuanMendes Si vous écrivez de bons tests et de petites unités, l'impact de l'effet que vous décrivez est faible dans la pratique.
Ant P
6

Je suis d'accord à 100% sur le fait que les tests unitaires nous aident "à affiner notre conception et nos éléments de refactorisation".

Je me demande bien si ils vous aident à faire la conception initiale . Oui, ils révèlent des défauts évidents et vous obligent à réfléchir à "comment puis-je rendre le code testable"? Cela devrait entraîner moins d'effets secondaires, une configuration et des paramétrages plus faciles, etc.

Cependant, selon mon expérience, des tests unitaires trop simplistes, rédigés avant que vous ne compreniez vraiment ce que la conception devrait être ((certes, c'est une exagération du TDD dur, mais trop souvent, les codeurs écrivent un test avant de trop réfléchir) - conduisent souvent à une anémie. modèles de domaine qui exposent trop d'internes.

Mon expérience avec TDD remonte à plusieurs années. Je suis donc intéressée par les nouvelles techniques qui pourraient aider à rédiger des tests qui ne biaisent pas trop la conception sous-jacente. Merci.

utilisateur949300
la source
Un grand nombre de paramètres de méthode sont une odeur de code et un défaut de conception.
Soufian
5

Le test unitaire vous permet de voir le fonctionnement des interfaces entre les fonctions et vous indique souvent comment 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 les tests unitaires), une fois les bogues découverts, vous pouvez ajouter des tests pour confirmer que les bogues sont corrigés.

J'utilise TDD pour certains de mes projets. Je déploie beaucoup d'efforts pour créer des exemples à partir de manuels ou de papiers considérés comme corrects, et tester le code que je développe à l'aide de ces exemples. Tous les malentendus que j'ai concernant les méthodes deviennent très évidents.

J'ai tendance à être un peu plus lâche que certains de mes collègues car je me fiche de savoir si le code est écrit en premier ou si le test est écrit en premier.

Robert Baron
la source
C'est une excellente réponse pour moi. Souhaitez-vous mettre quelques exemples, par exemple un pour chaque cas (lorsque vous obtenez un aperçu de la conception, etc.).
User039402
5

Lorsque vous voulez tester votre analyseur en détectant correctement la délimitation des valeurs, vous pouvez le transmettre à une ligne d'un fichier CSV. Pour que votre test soit direct et court, vous pouvez le tester avec une méthode qui accepte une ligne.

Cela vous fera automatiquement séparer la lecture des lignes de la lecture de valeurs individuelles.

À un autre niveau, vous ne souhaiterez peut-être pas placer toutes sortes de fichiers CSV physiques dans votre projet de test, mais faire quelque chose de plus lisible, il vous suffit de déclarer une grande chaîne CSV dans votre test pour en améliorer la lisibilité et le but. Cela vous amènera à découpler votre analyseur des E / S que vous feriez ailleurs.

Juste un exemple basique, commencez simplement à le pratiquer, vous sentirez la magie à un moment donné (je l’ai).

Joppe
la source
4

En termes simples, la rédaction de tests unitaires permet d’exposer les failles de votre code.

Ce guide spectaculaire pour l'écriture de code testable , écrit par Jonathan Wolter, Russ Ruffer et Miško Hevery, contient de nombreux exemples montrant comment les failles du code, qui empêchent les tests, empêchent également la réutilisation et la flexibilité du même code. Ainsi, si votre code est testable, il est plus facile à utiliser. La plupart des "valeurs morales" sont des astuces ridiculement simples qui améliorent considérablement la conception du code ( Dependency Injection 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. En effet, 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épendance, 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()); Notice, nous avons un contrôle plus nuancé de l'objet et celui-ci rapporte des dividendes immédiatement.

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 stockées dans une base de données ... il vous suffit de transmettre EXACTEMENT les données dont vous avez besoin.

Ivan
la source
2
Honnêtement, je ne suis pas sûr de savoir comment vous voulez dire l'exemple. Quel est le lien entre la méthode computeStuff et le cache?
John V
1
@ user970696 - Oui, je veux dire que "computeStuff ()" utilise le cache. La question est "Est-ce que computeStuff () fonctionne correctement tout le temps (ce qui dépend de l'état du cache)" Par conséquent, il est difficile de confirmer que computeStuff () fait ce que vous voulez POUR TOUTES LES ÉTATS DE CACHE POSSIBLES si vous ne pouvez pas définir directement / construisez le cache car vous avez codé en dur la ligne "cacheOfExpensiveComputation = buildBigCache ();" (au lieu de passer dans la cache directement via le constructeur)
Ivan
0

En fonction de ce que l’on entend par «tests unitaires», je ne pense pas que les tests unitaires vraiment de bas niveau facilitent autant la conception que les tests d’ intégration de niveau légèrement supérieur - des tests qui testent un groupe d’acteurs (classes, fonctions, peu importe) dans. votre code se combine correctement pour produire toute une série de comportements souhaitables convenus entre l’équipe de développement et le responsable du produit.

Si vous pouvez écrire des tests à ces niveaux, cela vous poussera à créer un code agréable, logique, semblable à une API, qui ne nécessite pas beaucoup de dépendances insensées - le désir d’avoir une configuration de test simple vous conduira naturellement à ne pas avoir beaucoup dépendances folles ou code étroitement couplé.

Ne vous y trompez pas: les tests unitaires peuvent vous mener à une mauvaise conception, ainsi qu’à une bonne conception.. J'ai vu des développeurs prendre un peu de code qui a déjà une belle conception logique et un seul problème, le séparer et introduire davantage d'interfaces uniquement à des fins de test, ce qui rend le code moins lisible et plus difficile à modifier. , et peut-être même plus de bogues si le développeur a décidé qu’avoir beaucoup de tests unitaires de bas niveau signifiait qu’ils n’avaient pas besoin de tests de niveau supérieur. Un exemple favori particulier est un bogue que j'ai corrigé où il y avait beaucoup de code "testable" très décomposé permettant d'obtenir des informations dans 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 lors des tests et autres trucs amusants. Un seul problème - il n'y avait pas de code qui a réellement interagi avec le mécanisme de presse-papiers du système d'exploitation,

Les tests unitaires peuvent certainement orienter votre conception - mais ils ne vous guident pas automatiquement vers une bonne conception. Vous devez avoir une idée de ce qu'est un bon design qui va au-delà: "ce code est testé, donc il est testable, donc c'est bon".

Bien sûr, si vous faites partie de ceux pour qui «tests unitaires» signifie «tous les tests automatisés qui ne sont pas pilotés par l'interface utilisateur», certains de ces avertissements risquent de ne pas être aussi pertinents. Les tests d’intégration de niveau supérieur sont souvent les plus utiles pour piloter votre conception.

topo réintègre Monica
la source
-2

Les tests unitaires peuvent aider à la refactorisation lorsque le nouveau code passe avec succès tous les anciens tests.

Supposons que vous ayez implémenté un bubbleort parce que vous étiez pressé et que vous ne vous préoccupiez pas des performances, mais vous souhaitez maintenant un tri rapide car les données s'allongent. Si tous les tests réussissent, les choses vont bien.

Bien sûr, les tests doivent être complets pour que cela fonctionne. Dans mon exemple, il se peut que vos tests ne couvrent pas la stabilité, car ce n’est pas un problème avec bubbleort.

om
la source
1
C’est vrai, mais c’est plus un avantage en termes de maintenabilité qu’un impact direct sur la qualité de la conception du code.
Ant P
@AntP, le PO a posé des questions sur le refactoring et les tests unitaires.
om
1
La question mentionnait la refactorisation, mais la question était de savoir comment les tests unitaires pourraient améliorer / vérifier la conception du code - et non pas faciliter le processus de refactorisation.
Ant P
-3

J'ai trouvé que les tests unitaires étaient très utiles pour faciliter la maintenance à long terme d'un projet. Lorsque je reviens sur un projet après des mois et que je ne me souviens pas beaucoup des détails, l'exécution de tests m'empêche de tout casser.

David Baker
la source
6
C'est certainement un aspect important des tests, mais cela ne répond pas vraiment à la question (ce n'est pas pourquoi les tests sont bons, mais comment ils influencent la conception).
Hulk