Types de tests unitaires basés sur l'utilité


13

Du point de vue de la valeur, je vois deux groupes de tests unitaires dans ma pratique:

  1. Tests qui testent une logique non triviale. Les écrire (avant l'implémentation ou après) révèle certains problèmes / bogues potentiels et aide à être sûr que la logique sera modifiée à l'avenir.
  2. Des tests qui testent une logique très triviale. Ces tests ressemblent plus à du code de document (généralement avec des simulations) qu'à le tester. Le flux de travail de maintenance de ces tests n'est pas "une logique a changé, le test est devenu rouge - Dieu merci, j'ai écrit ce test" mais "un code trivial a changé, le test est devenu faux négatif - je dois maintenir (réécrire) le test sans gagner aucun profit" . La plupart du temps, ces tests ne valent pas la peine d'être maintenus (sauf pour des raisons religieuses). Et d'après mon expérience dans de nombreux systèmes, ces tests représentent 80% de tous les tests.

J'essaie de savoir ce que les autres pensent du sujet des tests unitaires de séparation par valeur et comment cela correspond à ma séparation. Mais ce que je vois le plus, c'est la propagande TDD à plein temps ou les tests sont inutiles, il suffit d'écrire le code. Je m'intéresse à quelque chose au milieu. Vos propres pensées ou références à des articles / articles / livres sont les bienvenues.


3
Je garde les tests unitaires à la recherche de bogues connus (spécifiques) - qui une fois glissés dans l'ensemble de tests unitaires d'origine - en tant que groupe distinct dont le rôle est d'empêcher les bogues de régression.
Konrad Morawski

6
Ce deuxième type de tests est ce que je considère comme une sorte de «frottement de changement». Ne négligez pas leur utilité. Changer même les trivialités du code a tendance à avoir des effets d'entraînement dans toute la base de code, et introduire ce type de friction agit comme un obstacle pour vos développeurs afin qu'ils ne changent que les choses qui en ont vraiment besoin, plutôt que sur la base de fantaisies ou de préférences personnelles.
Telastyn

3
@Telastyn - Tout ce qui concerne votre commentaire me semble complètement fou. Qui rendrait délibérément difficile le changement de code? Pourquoi décourager les développeurs de modifier le code comme bon leur semble - ne leur faites-vous pas confiance? Sont-ils de mauvais développeurs?
Benjamin Hodgson

2
Dans tous les cas, si le changement de code a tendance à avoir des «effets d'entraînement», votre code a un problème de conception - auquel cas les développeurs doivent être encouragés à refactoriser autant que cela est raisonnable. Les tests fragiles découragent activement la refactorisation (un test échoue; qui peut être dérangé pour savoir si ce test était l'un des 80% des tests qui ne font vraiment rien? Vous trouvez simplement une manière différente et plus compliquée de le faire). Mais vous semblez considérer cela comme une caractéristique souhaitable ... Je ne comprends pas du tout.
Benjamin Hodgson

2
Quoi qu'il en soit, l'OP peut trouver intéressant cet article de blog du créateur de Rails. Pour simplifier excessivement son propos, vous devriez probablement essayer de jeter ces 80% de tests.
Benjamin Hodgson

Réponses:


14

Je pense qu'il est naturel de rencontrer une fracture dans les tests unitaires. Il existe de nombreuses opinions différentes sur la façon de le faire correctement et, naturellement, toutes les autres opinions sont intrinsèquement erronées . Il y a récemment pas mal d'articles sur DrDobbs qui explorent cette question à laquelle je fais un lien à la fin de ma réponse.

Le premier problème que je vois avec les tests est qu'il est facile de se tromper. Dans ma classe de collège C ++, nous avons été exposés à des tests unitaires au premier et au deuxième semestre. Nous ne savions rien de la programmation en général dans les deux semestres - nous essayions d'apprendre les principes fondamentaux de la programmation via C ++. Imaginez maintenant dire aux élèves: "Oh, vous avez écrit un petit calculateur d'impôt annuel! Maintenant, écrivez quelques tests unitaires pour vous assurer qu'il fonctionne correctement." Les résultats devraient être évidents - ils étaient tous horribles, y compris mes tentatives.

Une fois que vous admettez que vous aspirez à écrire des tests unitaires et que vous souhaitez vous améliorer, vous serez bientôt confronté à des styles de test à la mode ou à des méthodologies différentes. En testant les méthodologies, je me réfère à des pratiques telles que le test en premier ou ce que fait Andrew Binstock de DrDobbs, qui est d'écrire les tests à côté du code. Les deux ont leurs avantages et leurs inconvénients et je refuse d'entrer dans les détails subjectifs car cela inciterait à une guerre des flammes. Si vous n'êtes pas confus quant à la meilleure méthodologie de programmation, le style de test fera peut-être l'affaire. Devriez-vous utiliser TDD, BDD, les tests basés sur les propriétés? JUnit a des concepts avancés appelés Théories qui brouillent la frontière entre TDD et les tests basés sur les propriétés. Lequel utiliser quand?

tl; dr Il est facile de se tromper de test, il est incroyablement d'opinion et je ne pense pas qu'une méthodologie de test soit intrinsèquement meilleure tant qu'elle est utilisée avec diligence et professionnalisme dans le contexte dans lequel elle est appropriée. En outre, le test est dans mon esprit, une extension des assertions ou des tests d'intégrité qui assuraient une approche ad hoc rapide du développement qui est maintenant beaucoup, beaucoup plus facile.

Pour une opinion subjective, je préfère écrire des "phases" de tests, faute d'une meilleure phrase. J'écris des tests unitaires qui testent les classes de manière isolée, en utilisant des simulations si nécessaire. Celles-ci seront probablement exécutées avec JUnit ou quelque chose de similaire. Ensuite, j'écris des tests d'intégration ou d'acceptation, ceux-ci sont exécutés séparément et généralement seulement quelques fois par jour. Ce sont vos cas d'utilisation non triviaux. J'utilise généralement BDD car il est agréable d'exprimer des fonctionnalités en langage naturel, ce que JUnit ne peut pas facilement fournir.

Enfin, les ressources. Ceux-ci présenteront des opinions contradictoires principalement centrées sur les tests unitaires dans différentes langues et avec différents cadres. Ils devraient présenter le fossé idéologique et méthodologique tout en vous permettant de vous faire votre propre opinion tant que je n'ai pas déjà trop manipulé le vôtre :)

[1] La corruption de l'Agile par Andrew Binstock

[2] Réponse aux réponses de l'article précédent

[3] Réponse à la corruption d'Agile par l'oncle Bob

[4] Réponse à la corruption d'Agile par Rob Myers

[5] Pourquoi s'embêter avec les tests de concombre?

[6] Vous vous trompez

[7] Éloignez-vous des outils

[8] Commentaire sur «Kata de chiffres romains avec commentaire»

[9] Chiffres romains Kata avec commentaire


1
Une de mes affirmations amicales serait que si vous écrivez un test pour tester la fonction d'un calculateur d'impôt annuel, alors vous n'écrivez pas un test unitaire. C'est un test d'intégration. Votre calculatrice doit être décomposée en unités d'exécution assez simples, et vos tests unitaires testent ensuite ces unités. Si l'une de ces unités cesse de fonctionner correctement (le test commence à échouer), cela revient à assommer une partie d'un mur de fondation et vous devez réparer le code (pas le test, en général). Soit cela, soit vous avez identifié un morceau de code qui n'est plus nécessaire et doit être supprimé.
Craig

1
@Craig: Précisément! C'est ce que je voulais dire de ne pas savoir comment écrire les tests appropriés. En tant qu'étudiant, le collecteur d'impôts était une grande classe écrite sans bonne compréhension de SOLID. Vous avez tout à fait raison de penser qu'il s'agit plus d'un test d'intégration qu'autre chose, mais c'était un terme inconnu pour nous. Nous n'avons été exposés qu'à des tests "unitaires" par notre professeur.
IAE

5

Je pense qu'il est important d'avoir des tests des deux types et de les utiliser le cas échéant.

Comme vous l'avez dit, il y a deux extrêmes et je ne suis honnêtement pas d'accord avec l'un ou l'autre.

La clé est que les tests unitaires doivent couvrir les règles et les exigences commerciales . S'il est nécessaire que le système suive l'âge d'une personne, écrivez des tests «triviaux» pour vous assurer que l'âge est un entier non négatif. Vous testez le domaine de données requis par le système: bien que trivial, il a de la valeur car il applique les paramètres du système .

De même avec des tests plus complexes, ils doivent apporter de la valeur. Bien sûr, vous pouvez écrire un test qui valide quelque chose qui n'est pas une exigence mais qui devrait être appliqué dans une tour d'ivoire quelque part, mais il vaut mieux passer du temps à écrire des tests qui valident les exigences pour lesquelles le client vous paie. Par exemple, pourquoi écrire un test qui valide votre code peut traiter un flux d'entrée qui expire, lorsque les seuls flux proviennent de fichiers locaux, pas du réseau?

Je crois fermement aux tests unitaires et j'utilise TDD partout où cela a du sens. Les tests unitaires apportent certainement de la valeur sous la forme d'une qualité accrue et d'un comportement «rapide» lors du changement de code. Cependant, il y a aussi l'ancienne règle 80/20 à garder à l'esprit. À un certain point, vous obtiendrez des rendements décroissants lors de l'écriture de tests, et vous devrez passer à un travail plus productif même s'il y a une valeur mesurable à tirer de l'écriture de plus de tests.


La rédaction d'un test pour s'assurer qu'un système suit l'âge d'une personne n'est pas un test unitaire, OMI. C'est un test d'intégration. Un test unitaire testerait l'unité générique d'exécution (alias "procédure") qui, par exemple, calcule une valeur d'âge à partir, disons, d'une date de base et d'un décalage dans toutes les unités (jours, semaines, etc.). Mon point est que peu de code ne devrait pas avoir de dépendances sortantes étranges sur le reste du système. Il calcule JUSTEMENT un âge à partir de quelques valeurs d'entrée, et dans ce cas, un test unitaire peut confirmer le comportement correct, ce qui est probablement de lever une exception si l'offset produit un âge négatif.
Craig

Je ne parlais d'aucun calcul. Si un modèle stocke une donnée, il peut valider que les données appartiennent au domaine correct. Dans ce cas, le domaine est l'ensemble des entiers non négatifs. Les calculs doivent avoir lieu dans le contrôleur (dans MVC), et dans cet exemple, un calcul de l'âge serait un test distinct.

4

Voici mon point de vue: tous les tests ont des coûts:

  • temps et effort initiaux:
    • pensez à quoi tester et comment le tester
    • mettre en œuvre le test et assurez-vous qu'il teste ce qu'il est censé
  • maintenance en cours
    • s'assurer que le test fait toujours ce qu'il est censé faire car le code évolue naturellement
  • exécuter le test
    • temps d'exécution
    • analyser les résultats

Nous avons également l'intention que tous les tests fournissent des avantages (et d'après mon expérience, presque tous les tests fournissent des avantages):

  • spécification
  • mettre en évidence les cas d'angle
  • empêcher la régression
  • vérification automatique
  • exemples d'utilisation des API
  • quantification de propriétés spécifiques (temps, espace)

Il est donc assez facile de voir que si vous écrivez un tas de tests, ils auront probablement une certaine valeur. Lorsque cela devient compliqué, c'est lorsque vous commencez à comparer cette valeur (que, en passant, vous ne savez peut-être pas à l'avance - si vous jetez votre code, les tests de régression perdent leur valeur) au coût.

Maintenant, votre temps et vos efforts sont limités. Vous souhaitez choisir de faire les choses qui offrent le plus d'avantages au moindre coût. Et je pense que c'est une chose très difficile à faire, notamment parce que cela peut nécessiter des connaissances que l'on n'a pas ou qui coûteraient cher à obtenir.

Et c'est le vrai hic entre ces différentes approches. Je pense qu'ils ont tous identifié des stratégies de test qui sont bénéfiques. Cependant, chaque stratégie a des coûts et des avantages différents en général. De plus, les coûts et avantages de chaque stratégie dépendront probablement fortement des spécificités du projet, du domaine et de l'équipe. En d'autres termes, il peut y avoir plusieurs meilleures réponses.

Dans certains cas, le pompage de code sans tests peut fournir les meilleurs avantages / coûts. Dans d'autres cas, une suite de tests approfondie peut être préférable. Dans d'autres cas encore, l'amélioration de la conception peut être la meilleure chose à faire.


2

Qu'est - ce qu'un test unitaire , vraiment? Et y a-t-il vraiment une si grande dichotomie en jeu ici?

Nous travaillons dans un domaine où la lecture littéralement un bit après la fin d'un tampon peut totalement planter un programme, ou le faire produire un résultat totalement inexact, ou comme en témoigne le récent bogue TLS "HeartBleed", étendre un système censé être sécurisé à l'échelle ouvert sans produire de preuve directe de la faille.

Il est impossible d'éliminer toute complexité de ces systèmes. Mais notre travail consiste, dans la mesure du possible, à minimiser et à gérer cette complexité.

Un test unitaire est-il un test qui confirme, par exemple, qu'une réservation a bien été publiée dans trois systèmes différents, une entrée de journal est créée et une confirmation par e-mail est envoyée?

Je vais dire non . C'est un test d' intégration . Et ceux-ci ont certainement leur place, mais ils sont aussi un sujet différent.

Un test d'intégration fonctionne pour confirmer la fonction globale d'une «fonctionnalité» entière. Mais le code derrière cette fonctionnalité doit être décomposé en blocs de construction simples et testables, alias «unités».

Un test unitaire devrait donc avoir une portée très limitée.

Ce qui implique que le code testé par le test unitaire devrait avoir une portée très limitée.

Ce qui implique en outre que l'un des piliers d'une bonne conception consiste à décomposer votre problème complexe en morceaux plus petits et à usage unique (dans la mesure du possible) qui peuvent être testés relativement isolés les uns des autres.

Vous vous retrouvez avec un système constitué de composants de base fiables, et vous savez si l'une de ces unités fondamentales de code casse parce que vous avez écrit des tests simples, petits et à portée limitée pour vous dire exactement cela.

Dans de nombreux cas, vous devriez aussi probablement avoir plusieurs tests par unité. Les tests eux-mêmes doivent être simples, testant un et un seul comportement dans la mesure du possible.

La notion de «test unitaire» testant une logique non triviale, élaborée et complexe est, je pense, un peu un oxymore.

Donc, si ce genre de panne délibérée de la conception a eu lieu, comment un test unitaire pourrait-il soudainement commencer à produire des faux positifs, à moins que la fonction de base de l'unité de code testée n'ait changé? Et si cela s'est produit, vous feriez mieux de croire qu'il y a des effets d'entraînement non évidents en jeu. Votre test cassé, celui qui semble produire un faux positif, vous avertit en fait que certains changements ont brisé un cercle plus large de dépendances dans la base de code, et qu'il doit être examiné et corrigé.

Certaines de ces unités (beaucoup d'entre elles) peuvent avoir besoin d'être testées en utilisant des objets fictifs, mais cela ne signifie pas que vous devez écrire des tests plus complexes ou plus élaborés.

Pour en revenir à mon exemple artificiel d'un système de réservation, vous ne pouvez vraiment pas être l' envoi de demandes hors d'une base de données de réservation en direct ou un service tiers (ou même une instance « dev » de celui - ci) chaque fois que vous unité tester votre code.

Vous utilisez donc des mocks qui présentent le même contrat d'interface. Les tests peuvent ensuite valider le comportement d'un morceau de code déterministe relativement petit. Vert tout au long du tableau vous indique ensuite que les blocs qui composent votre fondation ne sont pas brisés.

Mais la logique des tests unitaires individuels eux-mêmes reste aussi simple que possible.


1

Ceci est bien sûr, juste mon opinion, mais après avoir passé les derniers mois à apprendre la programmation fonctionnelle dans fsharp (provenant d'un arrière-plan C #), je me suis rendu compte de quelques choses.

Comme l'OP l'a indiqué, il existe généralement 2 types de «tests unitaires» que nous observons quotidiennement. Les tests qui couvrent les entrées et les sorties d'une méthode, qui sont généralement les plus utiles, mais sont difficiles à faire pour 80% du système, ce qui est moins sur les «algorithmes» et plus sur les «abstractions».

L'autre type, qui teste l'interactivité de l'abstraction, implique généralement la moquerie. À mon avis, ces tests sont principalement nécessaires en raison de la conception de votre application. Les omettre, et vous risquez des bogues étranges et du code de spagetti, parce que les gens ne pensent pas correctement à leur conception à moins qu'ils ne soient obligés de faire les tests d'abord (et même alors, gâchent généralement). Le problème n'est pas tant la méthodologie de test, mais la conception sous-jacente du système. La plupart des systèmes construits avec des langages impératifs ou OO ont une dépendance intrinsèque aux "effets secondaires" aka "Faites ceci, mais ne me dites rien". Lorsque vous comptez sur l'effet secondaire, vous devez le tester, car une exigence commerciale ou une opération en fait généralement partie.

Lorsque vous concevez votre système de manière plus fonctionnelle, où vous évitez de créer des dépendances sur les effets secondaires et évitez les changements d'état / le suivi grâce à l'immuabilité, cela vous permet de vous concentrer plus fortement sur les tests des «tenants et aboutissants», qui testent clairement plus l'action. et moins comment vous y arrivez. Vous serez étonné de ce que des choses comme l'immuabilité peuvent vous apporter en termes de solutions beaucoup plus simples aux mêmes problèmes, et lorsque vous ne dépendez plus des "effets secondaires", vous pouvez faire des choses comme la parallélisation et la programmation asynchrone sans presque aucun coût supplémentaire.

Depuis que j'ai commencé à coder dans Fsharp, je n'ai eu besoin d'aucune infrastructure de simulation pour quoi que ce soit, et j'ai même complètement abandonné ma dépendance à l'égard d'un conteneur IOC. Mes tests sont guidés par les besoins et la valeur de l'entreprise, et non sur les couches d'abstraction lourdes généralement nécessaires pour réaliser la composition dans une programmation impérative.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.