Comment écrire de «bons» tests unitaires?

61

Déclenché par ce fil , je pense (encore une fois) à l'idée d'utiliser enfin des tests unitaires dans mes projets. Quelques affiches disent: "Les tests sont cool, s’ils sont de bons tests". Ma question maintenant: quels sont les "bons" tests?

Dans mes applications, l'essentiel est souvent une sorte d'analyse numérique, qui dépend de grandes quantités de données observées et qui aboutit à une fonction d'ajustement qui peut être utilisée pour modéliser ces données. J'ai trouvé particulièrement difficile de construire des tests pour ces méthodes, car le nombre d'entrées et de résultats possibles est trop important pour tester tous les cas, et les méthodes elles-mêmes sont souvent assez longues et ne peuvent pas être refactorisées facilement sans sacrifier les performances. Je suis particulièrement intéressé par les "bons" tests pour ce type de méthode.

Jens
la source
8
Tout bon test unitaire ne devrait tester qu’une chose: s’il échoue, vous devez savoir exactement ce qui ne va pas.
Gablin
2
Lorsque vous avez de grandes quantités de données, l’essentiel est d’écrire des tests génériques pouvant prendre en charge des fichiers de données. Les fichiers de données doivent généralement contenir à la fois une entrée et un résultat attendu. Avec les frameworks de test xunit, vous pouvez générer des cas de test à la volée - un pour chaque échantillon de données.
Froderik
2
@ gablin "Si cela échoue, vous devez savoir exactement ce qui ne va pas" suggérerait que les tests avec plusieurs causes d'échec possibles sont acceptables, tant que vous pouvez déterminer la cause à partir de la sortie du test ...?
user253751
Personne ne semble avoir mentionné que les tests unitaires peuvent tester la durée de l'opération. Vous pouvez modifier votre code en fonction des performances, en vous assurant que le test unitaire vous indique s'il réussit ou non, en fonction du temps et des résultats.
CJ Dennis

Réponses:

52

L'art des tests unitaires a la signification suivante à propos des tests unitaires:

Un test unitaire doit avoir les propriétés suivantes:

  • Il devrait être automatisé et reproductible.
  • Il devrait être facile à mettre en œuvre.
  • Une fois écrit, il devrait rester pour une utilisation future.
  • Tout le monde devrait pouvoir l'exécuter.
  • Il devrait fonctionner en appuyant sur un bouton.
  • Ça devrait courir vite.

et ajoute ensuite qu'il devrait être entièrement automatisé, digne de confiance, lisible et maintenable.

Je vous recommande fortement de lire ce livre si vous ne l'avez pas déjà fait.

À mon avis, tout cela est très important, mais les trois derniers (dignes de confiance, lisibles et gérables), en particulier, comme si vos tests avaient ces trois propriétés, votre code les avait aussi.

Andy Lowry
la source
1
+1 pour une liste complète ciblant les tests unitaires (et non les tests d'intégration et fonctionnels)
Gary Rowe
1
+1 pour le lien. Matériel intéressant à trouver là-bas.
Joris Meys
1
"Courir vite" a de grandes implications. C'est l'une des raisons pour lesquelles les tests unitaires doivent être exécutés de manière isolée, loin des ressources externes telles que la base de données, le système de fichiers, le service Web, etc. Cela conduit à des répliques / stubs.
Michael Easter
1
quand il dit It should run at the push of a button, est - ce que cela signifie qu'un test unitaire ne doit pas exiger soit des conteneurs (serveur d'application) en cours d' exécution (pour l'unité testée) ou une connexion de ressources (comme DB, les services Web externes , etc.)? Je ne comprends pas très bien quelles parties d'une application doivent faire l'objet d'un test unitaire ou non. On m'a dit que les tests unitaires ne devraient pas dépendre de la connexion à la base de données et des conteneurs en cours d'exécution; il se peut que des maquettes soient utilisées.
Amphibient
42

Un bon test unitaire ne reflète pas la fonction testée.

A titre d'exemple très simplifié, considérons que vous avez une fonction qui renvoie une moyenne de deux int. Le test le plus complet appelle la fonction et vérifie si le résultat est en fait une moyenne. Cela n’a aucun sens: vous mettez en miroir (répliquez) la fonctionnalité que vous testez. Si vous faites une erreur dans la fonction principale, vous ferez la même erreur lors du test.

En d'autres termes, si vous vous retrouvez à reproduire la fonctionnalité principale du test unitaire, c'est probablement le signe que vous perdez votre temps.

Mojuba
la source
21
+1 Ce que vous feriez dans ce cas est de tester avec des arguments codés en dur et de vérifier votre réponse connue.
Michael K
J'ai déjà vu cette odeur.
Paul Butcher
Pourriez-vous donner un exemple de bon test unitaire pour la fonction qui renvoie des moyennes?
VLAS
2
Valeurs prédéfinies du test @VLAS, par exemple, assurez-vous que avg (1, 3) == 2, vérifiez également les cas de bords, tels que INT_MAX, zéros, valeurs négatives, etc. Si un bogue a été détecté et corrigé dans la fonction, ajoutez-en un autre. Testez pour vous assurer que ce bogue n'est jamais réintroduit.
Mojuba
Intéressant. Comment proposez-vous d'obtenir les réponses correctes à ces entrées de test et de ne pas potentiellement faire la même erreur que le code soumis au test?
Timo
10

Un bon test unitaire est essentiellement la spécification sous forme exécutable:

  1. décrire le comportement du code correspondant aux cas d'utilisation
  2. couvrir les cas techniques (que se passe-t-il si la valeur null est passée) - si aucun test n'est présent pour un cas, le comportement est indéfini.
  3. pause si le code testé change de la spécification

J'ai trouvé que Test-Driven-Development était très bien adapté aux routines de bibliothèque car vous écrivez essentiellement l'API d'abord, puis ALORS la mise en œuvre réelle.


la source
7

pour TDD, de "bons" tests, des fonctionnalités de test souhaitées par le client ; les fonctionnalités ne correspondent pas nécessairement aux fonctions, et le développeur ne doit pas créer des scénarios de test en vase clos

dans votre cas - je suppose - la «fonctionnalité» est que la fonction d'ajustement modélise les données d'entrée avec une certaine tolérance aux erreurs. Puisque je n'ai aucune idée de ce que vous faites réellement, je fabrique quelque chose; j'espère que c'est analgous.

Exemple d'histoire:

En tant que [pilote X-Wing], je souhaite [une erreur d'ajustement maximale de 0,0001%] afin que [l'ordinateur de ciblage puisse atteindre le port d'échappement de l'Étoile Étoile de la Mort lorsqu'il se déplace à toute vitesse dans un canyon en rangée]

Alors vous allez parler aux pilotes (et à l'ordinateur de ciblage, s'il est sensible). Tout d'abord, vous parlez de ce qui est «normal», puis de l'anormal. Vous découvrez ce qui compte vraiment dans ce scénario, ce qui est commun, ce qui est peu probable et ce qui est simplement possible.

Supposons que vous disposiez normalement d'une fenêtre d'une demi-seconde sur sept canaux de données de télémétrie: vitesse, tangage, roulis, lacet, vecteur cible, taille cible et vitesse cible, et que ces valeurs soient constantes ou changent linéairement. De manière anormale, vous pouvez avoir moins de canaux et / ou les valeurs peuvent changer rapidement. Alors, ensemble, vous obtenez des tests tels que:

//Scenario 1 - can you hit the side of a barn?
Given:
    all 7 channels with no dropouts for the full half-second window,
When:
    speed is zero
    and target velocity is zero
    and all other values are constant,
Then:
    the error coefficient must be zero

//Scenario 2 - can you hit a turtle?
Given:
    all 7 channels with no dropouts for the full half-second window,
When:
    speed is zero
    and target velocity is less than c
    and all other values are constant,
Then:
    the error coefficient must be less than 0.0000000001/ns

...

//Scenario 42 - death blossom
Given:
    all 7 channels with 30% dropout and a 0.05 second sampling window
When:
    speed is zero
    and position is within enemy cluster
    and all targets are stationary
Then:
    the error coefficient must be less than 0.000001/ns for each target

Vous avez peut-être remarqué qu'il n'y a pas de scénario pour la situation particulière décrite dans l'histoire. Après avoir discuté avec le client et d’autres parties prenantes, il s’avère que cet objectif n’était qu’un exemple hypothétique. Les vrais tests sont issus de la discussion qui a suivi. Cela peut arriver. L’histoire doit être réécrite, mais pas nécessairement [car l’histoire n’est qu’un espace réservé pour une conversation avec le client].

Steven A. Lowe
la source
5

Créez des tests pour les cas extrêmes, comme un ensemble de test contenant uniquement le nombre minimum d'entrées (1 ou 0 possible) et quelques cas standard. Ces tests unitaires ne remplacent pas non plus les tests de réception approfondis.

utilisateur281377
la source
5

J'ai vu de nombreux cas où des personnes investissent énormément d'effort pour écrire des tests de code rarement saisi et ne pas écrire de tests pour du code saisi fréquemment.

Avant de vous préparer à passer des tests, vous devriez examiner un graphique d’appel afin de vous assurer de planifier une couverture adéquate.

De plus, je ne crois pas à écrire des tests juste pour dire "Ouais, on teste ça". Si j'utilise une bibliothèque qui est restée immuable et restera immuable, je ne vais pas perdre une journée à écrire des tests pour m'assurer que les entrailles d'une API qui ne changera jamais fonctionneront comme prévu, même si certaines parties de celle-ci marquent des points. haut sur un graphique d'appel. Les tests utilisant cette librairie (mon propre code) le prouvent.

Tim Post
la source
mais que se passera-t-il plus tard lorsque la bibliothèque aura une version plus récente avec un correctif de bogue?
@ Thorbjørn Ravn Andersen - Cela dépend de la bibliothèque, de ce qui a changé et de son propre processus de test. Je ne vais pas écrire des tests pour le code qui, je le sais, fonctionne lorsque je l'ai laissé en place, sans jamais le toucher. Donc, si cela fonctionne après la mise à jour, ne vous en faites pas: :) Bien sûr, il y a des exceptions.
Tim Post
si vous dépendez de votre bibliothèque, le moins que vous pouvez faire est d'écrire des tests qui montrent ce que vous attendez ladite bibliothèque de réellement faire ,
... et si cela change, les tests sur les choses qui consomment ladite bibliothèque ... tl; dr; Je n'ai pas besoin de tester les entrailles du code tiers. La réponse a été mise à jour pour plus de clarté.
Tim Post
4

Pas tout à fait aussi TDD, mais une fois que vous êtes passé au contrôle qualité, vous pouvez améliorer vos tests en configurant des scénarios de test afin de reproduire tous les bogues rencontrés au cours du processus de contrôle qualité. Cela peut être particulièrement utile lorsque vous entrez dans un support à long terme et que vous commencez à vous rendre dans un endroit où vous risquez que des personnes réintroduisent par inadvertance d'anciens bugs. Avoir un test en place pour capturer cela est particulièrement précieux.

glénatron
la source
3

J'essaie de faire en sorte que chaque test ne teste qu'une chose. J'essaie de donner à chaque test un nom tel que shouldDoSomething (). J'essaie de tester le comportement, pas la mise en œuvre. Je teste uniquement des méthodes publiques.

J'ai habituellement un ou plusieurs tests de réussite, puis peut-être une poignée de tests d'échec, par méthode publique.

J'utilise beaucoup les maquettes. Un bon modèle de framework serait probablement très utile, comme PowerMock. Bien que je n'en utilise pas encore.

Si la classe A utilise une autre classe B, j'ajouterais une interface, X, de sorte que A n'utilise pas B directement. Ensuite, je créais une maquette XMockup et l’utilisais au lieu de B dans mes tests. Cela aide vraiment à accélérer l'exécution des tests, à réduire la complexité des tests et à réduire le nombre de tests que j'écris pour A, car je n'ai pas à faire face aux particularités de B. Je peux par exemple tester que A appelle X.someMethod () au lieu d'appeler B.someMethod ().

Gardez votre code de test propre aussi.

Lors de l'utilisation d'une API, telle qu'une couche de base de données, je la simulais et la permettais de générer une exception à chaque opportunité possible sur commande. Je lance ensuite les tests un sans lancer, puis dans une boucle, lançant à chaque fois une exception à la prochaine occasion jusqu'à ce que le test réussisse à nouveau. Un peu comme les tests de mémoire disponibles pour Symbian.

Roger CS Wernersson
la source
2

Je vois qu'Andry Lowry a déjà publié les métriques de tests unitaires de Roy Osherove; mais il semble que personne n'ait présenté l'ensemble (complémentaire) que donne Oncle Bob dans Clean Code (132-133). Il utilise l'acronyme FIRST (ici avec mes résumés):

  • Rapide (ils devraient courir vite, pour que les gens ne s'en fassent pas)
  • Indépendants (les tests ne doivent pas effectuer de configuration ou de démontage mutuel)
  • Répétable (devrait fonctionner sur tous les environnements / plates-formes)
  • Auto-validation (entièrement automatisée; le résultat doit être "réussi" ou "échoué", pas un fichier journal)
  • En temps opportun (quand les écrire, juste avant d'écrire le code de production qu'ils testent)
Kazark
la source