it-swarm.dev

Comment écrire de "bons" tests unitaires?

Déclenché par ce fil , je pense (encore) à utiliser enfin des tests unitaires dans mes projets. Quelques affiches là-bas disent quelque chose comme "Les tests sont cool, si ce sont de bons tests". Ma question maintenant: quels sont les "bons" tests?

Dans mes applications, la partie principale est souvent une sorte d'analyse numérique, en fonction de grandes quantités de données observées, et résultant en 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 simplement tester chaque cas, et les méthodes elles-mêmes sont souvent assez longues et ne peuvent pas être facilement refactorisées sans sacrifier les performances. Je suis particulièrement intéressé par les "bons" tests pour ce type de méthode.

63
Jens

L'art des tests unitaires a ce qui suit à propos des tests unitaires:

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

  • Il doit être automatisé et reproductible.
  • Il devrait être facile à mettre en œuvre.
  • Une fois écrit, il doit rester pour une utilisation future.
  • Tout le monde devrait pouvoir l'exécuter.
  • Il devrait fonctionner sur simple pression d'un bouton.
  • Il devrait fonctionner rapidement.

puis ajoute plus tard qu'il doit être entièrement automatisé, fiable, lisible et maintenable.

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

À mon avis, tous ces éléments sont très importants, mais les trois derniers (dignes de confiance, lisibles et maintenables) en particulier, comme si vos tests ont ces trois propriétés, votre code les possède généralement également.

52
Andy Lowry

Un bon test unitaire ne reflète pas la fonction qu'il teste.

À titre d'exemple grandement simplifié, considérez que vous avez une fonction qui renvoie une moyenne de deux int. Le test le plus complet appellerait la fonction et vérifierait si un 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 avez fait une erreur dans la fonction principale, vous ferez la même erreur dans le test.

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

43
mojuba

De bons tests unitaires sont essentiellement la spécification sous forme exécutable:

  1. décrire le comportement du code correspondant aux cas d'utilisation
  2. couvrir les cas d'angle techniques (ce qui se passe si null est passé) - si un test n'est pas présent pour un cas d'angle, le comportement n'est pas défini.
  3. casser si le code testé change loin de la spécification

J'ai trouvé que le développement piloté par les tests était très bien adapté aux routines de bibliothèque, car vous écrivez d'abord l'API, puis la mise en œuvre réelle.

10
user1249

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

dans votre cas - je suppose - la "caractéristique" est que la fonction d'ajustement modélise les données d'entrée dans une certaine tolérance d'erreur. Puisque je n'ai aucune idée de ce que vous faites vraiment, je fais quelque chose; j'espère que c'est analgique.

Exemple d'histoire:

En tant que [Pilote X-Wing], je veux [pas plus de 0,0001% d'erreur d'ajustement] afin que [l'ordinateur de ciblage puisse toucher le port d'échappement de l'Étoile de la Mort lorsqu'il se déplace à pleine vitesse dans un canyon de boîte]

Vous allez donc parler aux pilotes (et à l'ordinateur de ciblage, s'il est sensible). Vous parlez d'abord 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.

Disons que normalement, vous aurez 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 seront constantes ou changent linéairement. Anormalement, vous pouvez avoir moins de canaux et/ou les valeurs peuvent changer rapidement. Alors ensemble vous venez avec 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

Maintenant, vous avez peut-être remarqué qu'il n'y a pas de scénario pour la situation particulière décrite dans l'histoire. Il s'avère, après avoir parlé avec le client et d'autres parties prenantes, que l'objectif de l'histoire originale n'était qu'un exemple hypothétique. Les vrais tests sont sortis de la discussion qui a suivi. Cela peut arriver. L'histoire doit être réécrite, mais elle ne doit pas l'être [puisque l'histoire n'est qu'un espace réservé pour une conversation avec le client].

7
Steven A. Lowe

Créez des tests pour les cas d'angle, comme un jeu de tests contenant uniquement le nombre minimum d'entrées (1 ou 0 possible) et quelques cas standard. Ces tests unitaires ne remplacent pas les tests d'acceptation approfondis, et ne devraient pas l'être.

5
user281377

J'ai vu beaucoup de cas où les gens investissent énormément d'efforts pour écrire des tests pour du code qui est rarement entré, et pas pour écrire des tests pour du code qui est entré fréquemment.

Avant de vous asseoir pour écrire des tests, vous devriez regarder une sorte de graphique d'appel, pour 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 déposée 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 haut sur un graphique d'appel. Teste que consommer ladite bibliothèque (mon propre code) le montre.

5
Tim Post

Pas tout à fait comme TDD, mais une fois que vous êtes entré dans le contrôle qualité, vous pouvez améliorer vos tests en configurant des cas de test pour reproduire les bogues qui surviennent pendant le processus d'assurance qualité. Cela peut être particulièrement utile lorsque vous accédez à un support à plus long terme et que vous commencez à arriver à un endroit où vous risquez que des gens réintroduisent par inadvertance d'anciens bogues. Avoir un test en place pour capturer cela est particulièrement précieux.

4
glenatron

J'essaie de faire en sorte que chaque test ne teste qu'une seule chose. J'essaie de donner à chaque test un nom comme shouldDoSomething (). J'essaie de tester le comportement, pas l'implémentation. Je teste uniquement les méthodes publiques.

J'ai généralement un ou quelques tests de réussite, puis peut-être quelques tests d'échec, par méthode publique.

J'utilise beaucoup de maquettes. Un bon faux cadre 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, pour que A n'utilise pas directement B. Ensuite, je créerais une maquette XMockup et l'utiliserais 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 également 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'un effet secondaire d'appeler B.someMethod ().

Gardez également votre code de test propre.

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

3

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

  • Fast (ils devraient fonctionner rapidement, donc les gens ne voudront pas les exécuter)
  • Indépendant (les tests ne doivent pas faire de configuration ou de démontage les uns pour les autres)
  • répétable (devrait fonctionner sur tous les environnements/plates-formes)
  • Auto-validation (entièrement automatisé; la sortie doit être "réussite" ou "échec", pas un fichier journal)
  • en temps opportun (quand les écrire - juste avant d'écrire le code de production qu'ils testent)
2
Kazark