Quelle est la profondeur de vos tests unitaires?


88

Ce que j'ai trouvé à propos de TDD, c'est qu'il faut du temps pour mettre en place vos tests et, étant naturellement paresseux, je veux toujours écrire le moins de code possible. La première chose que je semble faire est de tester mon constructeur a défini toutes les propriétés, mais est-ce exagéré?

Ma question est à quel niveau de granularité écrivez-vous vos tests unitaires?

.. et y a-t-il trop de tests?

Réponses:


221

Je suis payé pour du code qui fonctionne, pas pour des tests, donc ma philosophie est de tester le moins possible pour atteindre un niveau de confiance donné (je soupçonne que ce niveau de confiance est élevé par rapport aux normes de l'industrie, mais cela pourrait simplement être de l'orgueil) . Si je ne fais généralement pas une sorte d'erreur (comme définir les mauvaises variables dans un constructeur), je ne la teste pas. J'ai tendance à donner un sens aux erreurs de test, donc je suis très prudent lorsque j'ai une logique avec des conditions compliquées. Lors du codage en équipe, je modifie ma stratégie pour tester soigneusement le code que nous avons, collectivement, tendance à nous tromper.

Différentes personnes auront des stratégies de test différentes basées sur cette philosophie, mais cela me semble raisonnable étant donné l'état immature de la compréhension de la façon dont les tests peuvent s'intégrer au mieux dans la boucle interne du codage. Dans dix ou vingt ans, nous aurons probablement une théorie plus universelle des tests à écrire, des tests à ne pas écrire et comment faire la différence. En attendant, l'expérimentation semble de mise.


40
Le monde ne pense pas que Kent Beck dirait cela! Il y a des légions de développeurs qui poursuivent consciencieusement une couverture à 100% parce qu'ils pensent que c'est ce que ferait Kent Beck! J'ai dit à beaucoup que vous avez dit, dans votre livre XP, que vous n'adhérez pas toujours religieusement à Test First. Mais je suis surpris aussi.
Charlie Flowers

6
En fait, je ne suis pas d'accord, car le code produit par un développeur n'est pas le sien, et le prochain sprint, quelqu'un d'autre le changera et commettra des erreurs que vous «savez que vous ne faites pas». Aussi TDD vous pensez d'abord aux tests. Donc, si vous faites TDD en supposant tester une partie du code, vous le faites mal
Ricardo Rodrigues

2
Je ne suis pas intéressé par la couverture. Je suis très intéressé par la fréquence à laquelle M. Beck commet du code qui n'a pas été écrit en réponse à un test échoué.
sheldonh

1
@RicardoRodrigues, vous ne pouvez pas écrire de tests pour couvrir du code que d'autres personnes écriront plus tard. C'est leur responsabilité.
Kief

2
Ce n'est pas ce que j'ai écrit, lu attentivement; J'ai écrit que si vous écrivez des tests pour ne couvrir qu'une partie de votre propre code, en laissant des parties non couvertes où "vous savez que vous ne faites pas d'erreurs" et que ces parties sont modifiées et n'ont pas de tests appropriés, vous avez un problème juste là, et ce n'est pas du tout TDD.
Ricardo Rodrigues

20

Écrivez des tests unitaires pour les choses que vous prévoyez de casser et pour les cas extrêmes. Après cela, les cas de test doivent être ajoutés au fur et à mesure que les rapports de bogue arrivent - avant d'écrire le correctif du bogue. Le développeur peut alors être sûr que:

  1. Le bogue est corrigé;
  2. Le bogue ne réapparaîtra pas.

Selon le commentaire ci-joint, je suppose que cette approche de l'écriture de tests unitaires pourrait poser des problèmes, si de nombreux bogues sont, au fil du temps, découverts dans une classe donnée. C'est probablement là où la discrétion est utile - l'ajout de tests unitaires uniquement pour les bogues susceptibles de se reproduire, ou lorsque leur réapparition poserait de sérieux problèmes. J'ai trouvé qu'une mesure des tests d'intégration dans les tests unitaires peut être utile dans ces scénarios - tester le code plus haut des chemins de code peut couvrir les chemins de code plus bas.


Avec la quantité de bugs que j'écris, cela peut devenir un anti-pattern. Avec des centaines de tests sur du code où les choses ont échoué, cela peut signifier que vos tests deviennent illisibles et lorsque le moment est venu de réécrire ces tests, cela peut devenir une surcharge.
Johnno Nolan

@JohnNolan: La lisibilité des tests est-elle si importante? À mon humble avis, ce n'est pas le cas, du moins pour ces tests de régression spécifiques aux bogues. Si vous réécrivez fréquemment des tests, vous testez peut-être à un niveau trop bas - idéalement, vos interfaces devraient rester relativement stables même si vos implémentations changent, et vous devriez tester au niveau de l'interface (bien que je réalise que le monde réel ne l'est souvent pas) t comme ça ...: - /) Si vos interfaces changent de manière importante, je préférerais supprimer la plupart ou la totalité de ces tests spécifiques aux bogues plutôt que de les réécrire.
j_random_hacker

@j_random_hacker Oui, bien sûr, la lisibilité est importante. Les tests sont une forme de documentation et sont aussi importants que le code de production. Je suis d'accord pour dire que supprimer les tests pour les changements majeurs est une bonne chose (tm) et que les tests doivent être effectués au niveau de l'interface.
Johnno Nolan

19

Tout doit être rendu aussi simple que possible, mais pas plus simple. - A. Einstein

L'une des choses les plus mal comprises à propos du TDD est le premier mot. Tester. C'est pourquoi BDD est venu. Parce que les gens ne comprenaient pas vraiment que le premier D était le plus important, à savoir Driven. Nous avons tous tendance à penser un peu trop aux tests et un peu à peu à la conduite du design. Et je suppose que c'est une réponse vague à votre question, mais vous devriez probablement considérer comment piloter votre code, au lieu de ce que vous testez réellement; c'est quelque chose qu'un outil de couverture peut vous aider. Le design est un problème plus important et plus problématique.


Ouais, c'est vague ... Cela signifie-t-il qu'un constructeur ne fait pas partie d'un comportement, nous ne devrions pas le tester. Mais je devrais tester MyClass.DoSomething ()?
Johnno Nolan

Eh bien, cela dépend de: P ... un test de construction est souvent un bon début pour essayer de tester du code hérité. Mais je laisserais probablement (dans la plupart des cas) un test de construction lorsque je commencerais à concevoir quelque chose à partir de zéro.
kitofr

C'est un développement piloté, pas une conception pilotée. Cela signifie, obtenir une base de référence fonctionnelle, écrire des tests pour vérifier la fonctionnalité, avancer dans le développement. J'écris presque toujours mes tests juste avant de factoriser du code pour la première fois.
Evan Plaice

Je dirais que le dernier D, Design, est le mot que les gens oublient, perdant ainsi leur concentration. Dans la conception pilotée par les tests, vous écrivez du code en réponse à des tests échoués. Si vous faites de la conception pilotée par les tests, quelle quantité de code non testé vous retrouvez-vous?
sheldonh

15

À ceux qui proposent de tester "tout": réalisez que "tester complètement" une méthode comme celle-ci int square(int x)nécessite environ 4 milliards de cas de test dans des langages communs et des environnements typiques.

En fait, il est encore pire que cela: une méthode void setX(int newX)est également obligé non de modifier les valeurs des autres membres en plus x- vous testons que obj.y, obj.zetc. tous demeurent inchangées après l' appel obj.setX(42);?

Il est seulement pratique de tester un sous-ensemble de «tout». Une fois que vous acceptez cela, il devient plus acceptable d'envisager de ne pas tester un comportement incroyablement basique. Chaque programmeur a une distribution de probabilité des emplacements des bogues; L'approche intelligente consiste à concentrer votre énergie sur les régions de test où vous estimez que la probabilité de bogue est élevée.


9

La réponse classique est "tester tout ce qui pourrait éventuellement casser". J'interprète cela comme signifiant que tester des setters et des getters qui ne font rien d'autre que set ou get est probablement trop de tests, pas besoin de prendre le temps. À moins que votre IDE ne les écrive pour vous, alors vous pourriez aussi bien.

Si votre constructeur ne définissant pas les propriétés peut entraîner des erreurs plus tard, il n'est pas exagéré de tester qu'elles sont définies.


yup et c'est une liaison pour une classe avec de nombreuses propriétés et de nombreux consturcteurs.
Johnno Nolan

Plus un problème est trivial (comme oublier d'initialiser un membre à zéro), plus il faudra de temps pour le déboguer.
Lev

5

J'écris des tests pour couvrir les hypothèses des classes que j'écrirai. Les tests appliquent les exigences. Essentiellement, si x ne peut jamais être 3, par exemple, je vais m'assurer qu'il y a un test qui couvre cette exigence.

Invariablement, si je n'écris pas de test pour couvrir une condition, cela reviendra plus tard pendant les tests "humains". Je vais certainement en écrire un alors, mais je préfère les attraper tôt. Je pense que le fait est que les tests sont fastidieux (peut-être) mais nécessaires. J'écris suffisamment de tests pour être complets, mais pas plus.


5

Une partie du problème avec le fait de sauter des tests simples maintenant est que dans le futur, la refactorisation pourrait rendre cette propriété simple très compliquée avec beaucoup de logique. Je pense que la meilleure idée est que vous pouvez utiliser des tests pour vérifier les exigences du module. Si lorsque vous réussissez X, vous devez récupérer Y, alors c'est ce que vous voulez tester. Ensuite, lorsque vous modifiez le code plus tard, vous pouvez vérifier que X vous donne Y, et vous pouvez ajouter un test pour A vous donne B, lorsque cette exigence est ajoutée plus tard.

J'ai trouvé que le temps que je passe pendant les tests de développement initial à écrire est payant dans le premier ou le deuxième correctif de bogue. La possibilité de récupérer du code que vous n'avez pas regardé depuis 3 mois et d'être raisonnablement sûr que votre correctif couvre tous les cas, et "probablement" ne casse rien est extrêmement précieux. Vous constaterez également que les tests unitaires aideront à trier les bogues bien au-delà de la trace de la pile, etc.


4

Dans la plupart des cas, je dirais que s'il y a de la logique, testez-la. Cela inclut les constructeurs et les propriétés, en particulier lorsque plusieurs éléments sont définis dans la propriété.

En ce qui concerne trop de tests, c'est discutable. Certains diraient que tout devrait être testé pour la robustesse, d'autres disent que pour des tests efficaces, seules les choses qui pourraient casser (c'est-à-dire la logique) devraient être testées.

Je me pencherais davantage vers le deuxième camp, juste par expérience personnelle, mais si quelqu'un décidait de tout tester, je ne dirais pas que c'était trop ... un peu exagéré peut-être pour moi, mais pas trop pour eux.

Donc, non - je dirais qu'il n'y a pas de «trop» de tests au sens général, seulement pour les individus.


3

Le développement piloté par les tests signifie que vous arrêtez de coder lorsque tous vos tests réussissent.

Si vous n'avez pas de test pour une propriété, pourquoi devriez-vous la mettre en œuvre? Si vous ne testez / définissez pas le comportement attendu en cas d'affectation «illégale», que doit faire la propriété?

Par conséquent, je suis totalement pour tester chaque comportement qu'une classe devrait présenter. Y compris les propriétés "primitives".

Pour faciliter ce test, j'ai créé un NUnit simple TestFixturequi fournit des points d'extension pour définir / obtenir la valeur et prend des listes de valeurs valides et non valides et dispose d'un seul test pour vérifier si la propriété fonctionne correctement. Tester une seule propriété pourrait ressembler à ceci:

[TestFixture]
public class Test_MyObject_SomeProperty : PropertyTest<int>
{

    private MyObject obj = null;

    public override void SetUp() { obj = new MyObject(); }
    public override void TearDown() { obj = null; }

    public override int Get() { return obj.SomeProperty; }
    public override Set(int value) { obj.SomeProperty = value; }

    public override IEnumerable<int> SomeValidValues() { return new List() { 1,3,5,7 }; }
    public override IEnumerable<int> SomeInvalidValues() { return new List() { 2,4,6 }; }

}

En utilisant des lambdas et des attributs, cela pourrait même être écrit de manière plus compacte. Je suppose que MBUnit a même un support natif pour des choses comme ça. Le fait est que le code ci-dessus capture l'intention de la propriété.

PS: Probablement le PropertyTest devrait également avoir un moyen de vérifier que les autres propriétés de l'objet n'ont pas changé. Hmm ... retour à la planche à dessin.


Je suis allé à une présentation sur mbUnit. Cela semble très bien.
Johnno Nolan le

Mais David, laissez-moi vous demander: avez-vous été surpris par la réponse de Kent Beck ci-dessus? Sa réponse vous amène-t-elle à vous demander si vous devriez repenser votre approche? Pas parce que n'importe qui a des «réponses d'en haut», bien sûr. Mais Kent est considéré comme l'un des principaux promoteurs du test. Penny pour vos pensées!
Charlie Flowers

@Charly: La réponse de Kent est très pragmatique. Je travaille «juste» sur un projet dans lequel j'intégrerai du code provenant de diverses sources et j'aimerais apporter un très haut niveau de confiance.
David Schmitt

Cela dit, je m'efforce d'avoir des tests plus simples que le code testé et ce niveau de détail ne vaut peut-être la peine que dans les tests d'intégration où tous les générateurs, modules, règles métier et validateurs sont réunis.
David Schmitt

1

Je fais des tests unitaires pour atteindre la couverture maximale possible. Si je ne parviens pas à atteindre un code, je refactorise jusqu'à ce que la couverture soit aussi complète que possible

Après avoir terminé le test d'écriture aveuglant, j'écris généralement un cas de test reproduisant chaque bogue

J'ai l'habitude de faire la distinction entre les tests de code et les tests d'intégration. Pendant les tests d'intégration (qui sont également des tests unitaires, mais sur des groupes de composants, donc pas exactement à quoi servent les tests unitaires), je vais tester les exigences à implémenter correctement.


1

Donc plus je pilote ma programmation en écrivant des tests, moins je me soucie du niveau de granularité des tests. Avec le recul, il semble que je fais la chose la plus simple possible pour atteindre mon objectif de validation du comportement . Cela signifie que je génère une couche de confiance que mon code fait ce que je demande de faire, mais cela n'est pas considéré comme une garantie absolue que mon code est exempt de bogues. Je pense que le bon équilibre est de tester le comportement standard et peut-être un cas de bord ou deux, puis de passer à la partie suivante de ma conception.

J'accepte que cela ne couvre pas tous les bogues et j'utilise d'autres méthodes de test traditionnelles pour les capturer.


0

En général, je commence petit, avec des entrées et des sorties dont je sais qu'elles doivent fonctionner. Ensuite, à mesure que je corrige des bogues, j'ajoute plus de tests pour m'assurer que les choses que j'ai corrigées sont testées. C'est organique et ça marche bien pour moi.

Pouvez-vous tester trop? Probablement, mais il est probablement préférable de faire preuve de prudence en général, même si cela dépendra de l'importance de votre application.


0

Je pense que vous devez tout tester dans votre «cœur» de votre logique métier. Getter et Setter aussi car ils pourraient accepter une valeur négative ou une valeur nulle que vous ne voudriez peut-être pas accepter. Si vous avez le temps (dépend toujours de votre patron), il est bon de tester d'autres logiques métier et tous les contrôleurs qui appellent ces objets (vous passez lentement du test unitaire au test d'intégration).


0

Je ne teste pas les méthodes simples setter / getter qui n'ont pas d'effets secondaires. Mais je fais des tests unitaires pour toutes les autres méthodes publiques. J'essaye de créer des tests pour toutes les conditions aux limites dans mes algorthims et de vérifier la couverture de mes tests unitaires.

C'est beaucoup de travail mais je pense que ça vaut le coup. Je préférerais écrire du code (même tester le code) que parcourir le code dans un débogueur. Je trouve que le cycle code-build-deploy-debug prend beaucoup de temps et plus les tests unitaires que j'ai intégrés dans ma build sont exhaustifs, moins je passe de temps à parcourir ce cycle code-build-deploy-debug.

Vous n'avez pas dit pourquoi vous codez aussi l'architecture. Mais pour Java, j'utilise Maven 2 , JUnit , DbUnit , Cobertura et EasyMock .


Je n'ai pas dit que c'était une question assez indépendante de la langue.
Johnno Nolan

Les tests unitaires dans TDD ne vous couvrent pas seulement lorsque vous écrivez le code, ils vous protègent également contre la personne qui hérite de votre code et pense ensuite qu'il est logique de formater une valeur dans le getter!
Paxic le

0

Plus j'en lis à ce sujet, plus je pense que certains tests unitaires sont comme certains modèles: Une odeur de langues insuffisantes.

Lorsque vous devez tester si votre getter trivial renvoie réellement la bonne valeur, c'est parce que vous pouvez mélanger le nom du getter et le nom de la variable membre. Entrez «attr_reader: name» de ruby, et cela ne peut plus arriver. Tout simplement pas possible en java.

Si jamais votre getter devient non trivial, vous pouvez toujours ajouter un test pour cela.


Je suis d'accord que tester un getter est trivial. Cependant, je peux être assez stupide pour oublier de le définir dans un constructeur. Par conséquent, un test est nécessaire. Mes pensées ont changé depuis que j'ai posé la question. Voir ma réponse stackoverflow.com/questions/153234/how-deep-are-your-unit-tests/...
Johnno Nolan

1
En fait, je dirais que d'une certaine manière, les tests unitaires dans leur ensemble sont une odeur de problème de langue. Les langages qui prennent en charge les contrats (conditions pré / post sur les méthodes) comme Eiffel, ont encore besoin de quelques tests unitaires, mais ils en ont moins besoin. En pratique, même les contrats simples permettent de localiser très facilement les bogues: lorsque le contrat d'une méthode se rompt, le bogue se trouve généralement dans cette méthode.
Damien Pollet

@Damien: Peut-être que les tests unitaires et les contrats sont vraiment la même chose déguisé? Ce que je veux dire, c'est qu'un langage qui «prend en charge» les contrats permet simplement d'écrire facilement des extraits de code - des tests - qui sont (éventuellement) exécutés avant et après d'autres extraits de code, n'est-ce pas? Si sa grammaire est assez simple, un langage qui ne supporte pas nativement les contrats peut être facilement étendu pour les supporter en écrivant un préprocesseur, n'est-ce pas? Ou y a-t-il des choses qu'une approche (contrats ou tests unitaires) peut faire que l'autre ne peut tout simplement pas faire?
j_random_hacker

0

Testez le code source qui vous inquiète.

N'est pas utile de tester des portions de code dans lesquelles vous êtes très confiant, tant que vous ne faites pas d'erreurs.

Testez les corrections de bogues, de sorte que ce soit la première et la dernière fois que vous corrigez un bogue.

Testez pour obtenir la confiance des parties de code obscures, afin de créer des connaissances.

Testez avant une refactorisation lourde et moyenne, afin de ne pas casser les fonctionnalités existantes.


0

Cette réponse sert davantage à déterminer le nombre de tests unitaires à utiliser pour une méthode donnée que vous souhaitez effectuer un test unitaire en raison de sa criticité / importance. En utilisant la technique de test de base de McCabe, vous pouvez effectuer les opérations suivantes pour avoir une meilleure fiabilité de couverture de code que la simple «couverture des instructions» ou la «couverture des succursales»:

  1. Déterminez la valeur de complexité cyclomatique de votre méthode que vous souhaitez tester unitaire (Visual Studio 2010 Ultimate, par exemple, peut la calculer pour vous avec des outils d'analyse statique; sinon, vous pouvez la calculer à la main via la méthode flowgraph - http://users.csc. calpoly.edu/~jdalbey/206/Lectures/BasisPathTutorial/index.html )
  2. Répertoriez l'ensemble de base des chemins indépendants qui traversent votre méthode - voir le lien ci-dessus pour un exemple de diagramme de flux
  3. Préparer des tests unitaires pour chaque chemin de base indépendant déterminé à l'étape 2

Allez-vous faire cela pour chaque méthode? Sérieusement?
Kristopher Johnson
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.