Nouveau dans les tests unitaires, comment écrire de bons tests? [fermé]

267

Je suis relativement nouveau dans le monde des tests unitaires, et j'ai juste décidé d'ajouter une couverture de test pour mon application existante cette semaine.

C'est une tâche énorme, principalement en raison du nombre de classes à tester mais aussi parce que l'écriture de tests est nouvelle pour moi.

J'ai déjà écrit des tests pour un tas de classes, mais maintenant je me demande si je le fais bien.

Lorsque j'écris des tests pour une méthode, j'ai l'impression de réécrire une deuxième fois ce que j'ai déjà écrit dans la méthode elle-même.
Mes tests semblent tellement liés à la méthode (tester tous les chemins de code, en attendant que certaines méthodes internes soient appelées un certain nombre de fois, avec certains arguments), il semble que si je refactorise la méthode, les tests échoueront même si le le comportement final de la méthode n'a pas changé.

C'est juste un sentiment, et comme dit plus haut, je n'ai aucune expérience des tests. Si des testeurs plus expérimentés pouvaient me donner des conseils sur la façon d'écrire de bons tests pour une application existante, ce serait grandement apprécié.

Edit: Je serais ravi de remercier Stack Overflow, j'ai eu d'excellentes contributions en moins de 15 minutes qui ont répondu à plus d'heures de lecture en ligne que je viens de faire.

pixelastic
la source
1
C'est le meilleur livre pour les tests unitaires: manning.com/osherove Il explique toutes les meilleures pratiques, les choses à faire et à ne pas faire pour les tests unitaires.
Ervi B
Une chose que toutes ces réponses omettent, c'est que les tests unitaires sont comme de la documentation. Ergo, si vous écrivez une fonction, vous documenteriez son intention, en décrivant ses entrées et sorties (et, éventuellement, ses effets secondaires). Un test unitaire est destiné à vérifier cela, alors. Et si vous (ou quelqu'un d'autre) apportez ultérieurement des modifications au code, les documents doivent expliquer les limites des modifications qui peuvent être apportées, et les tests unitaires s'assurent que les limites sont respectées.
Thomas Tempelmann

Réponses:

187

Mes tests semblent tellement liés à la méthode (tester tous les chemins de code, en attendant que certaines méthodes internes soient appelées un certain nombre de fois, avec certains arguments), il semble que si je refactorise la méthode, les tests échoueront même si le le comportement final de la méthode n'a pas changé.

Je pense que vous vous trompez.

Un test unitaire doit:

  • tester une méthode
  • fournir des arguments spécifiques à cette méthode
  • tester que le résultat est comme prévu

Il ne doit pas regarder à l'intérieur de la méthode pour voir ce qu'elle fait, donc la modification des internes ne doit pas entraîner l'échec du test. Vous ne devez pas tester directement que des méthodes privées sont appelées. Si vous souhaitez savoir si votre code privé est testé, utilisez un outil de couverture de code. Mais ne soyez pas obsédé par cela: une couverture à 100% n'est pas une exigence.

Si votre méthode appelle des méthodes publiques dans d'autres classes et que ces appels sont garantis par votre interface, vous pouvez tester que ces appels sont effectués à l'aide d'un framework de simulation.

Vous ne devez pas utiliser la méthode elle-même (ou l'un des codes internes qu'elle utilise) pour générer dynamiquement le résultat attendu. Le résultat attendu doit être codé en dur dans votre scénario de test afin qu'il ne change pas lorsque l'implémentation change. Voici un exemple simplifié de ce que devrait faire un test unitaire:

testAdd()
{
    int x = 5;
    int y = -2;
    int expectedResult = 3;
    Calculator calculator = new Calculator();
    int actualResult = calculator.Add(x, y);
    Assert.AreEqual(expectedResult, actualResult);
}

Notez que la façon dont le résultat est calculé n'est pas vérifiée - seulement que le résultat est correct. Continuez à ajouter des cas de test de plus en plus simples comme ci-dessus jusqu'à ce que vous ayez couvert autant de scénarios que possible. Utilisez votre outil de couverture de code pour voir si vous avez manqué des chemins intéressants.

Mark Byers
la source
13
Merci beaucoup, votre réponse était la plus complète. Je comprends maintenant mieux à quoi servent réellement les objets fantômes: je n'ai pas besoin d'affirmer chaque appel à d'autres méthodes, juste celles qui sont pertinentes. Je n'ai pas non plus besoin de savoir COMMENT les choses se font, mais qu'elles le font correctement.
pixelastic
2
Je pense respectueusement que vous vous trompez. Les tests unitaires concernent le flux d'exécution de code (test en boîte blanche). Le test de la boîte noire (ce que vous proposez) est généralement la technique utilisée dans les tests fonctionnels (tests de système et tests d'intégration).
Wes
1
"Un test unitaire devrait tester une méthode" Je ne suis pas d'accord. Un test unitaire doit tester un concept logique. Bien que cela soit souvent représenté comme une méthode, ce n'est pas toujours le cas
robertmain
35

Pour les tests unitaires, j'ai trouvé que Test Driven (tests en premier, code en second) et code en premier, test en second lieu étaient extrêmement utiles.

Au lieu d'écrire du code, puis d'écrire test. Écrivez le code, puis regardez ce que vous pensez que le code devrait faire. Pensez à toutes les utilisations prévues et écrivez un test pour chacune. Je trouve que les tests d'écriture sont plus rapides mais plus complexes que le codage lui-même. Les tests devraient tester l'intention. Pensez également aux intentions que vous finissez par trouver des cas d'angle dans la phase d'écriture du test. Et bien sûr, lors de l'écriture de tests, l'une des rares utilisations peut provoquer un bogue (quelque chose que je trouve souvent, et je suis très heureux que ce bogue n'ait pas corrompu les données et ne soit pas contrôlé).

Pourtant, tester est presque comme coder deux fois. En fait, j'avais des applications où il y avait plus de code de test (quantité) que de code d'application. Un exemple était une machine d'état très complexe. Je devais m'assurer qu'après y avoir ajouté plus de logique, le tout fonctionnait toujours sur tous les cas d'utilisation précédents. Et comme ces cas étaient assez difficiles à suivre en regardant le code, j'ai fini par avoir une si bonne suite de tests pour cette machine que j'étais confiant qu'elle ne casserait pas même après avoir fait des changements, et les tests m'ont sauvé le cul plusieurs fois . Et comme les utilisateurs ou les testeurs trouvaient des bogues avec les cas de flux ou de coin manquants, devinez quoi, ajoutés aux tests et ne se reproduisaient plus jamais. Cela a vraiment donné aux utilisateurs confiance en mon travail en plus de rendre le tout super stable. Et quand il a dû être réécrit pour des raisons de performances, devinez quoi,

Tous les exemples simples function square(number)sont excellents et sont probablement de mauvais candidats pour passer beaucoup de temps à tester. Ceux qui font une logique métier importante, c'est là que les tests sont importants. Testez les exigences. Ne vous contentez pas de tester la plomberie. Si les exigences changent, devinez quoi, les tests doivent l'être aussi.

Le test ne doit pas littéralement tester cette fonction foo invoquée la barre de fonction 3 fois. C'est faux. Vérifiez si le résultat et les effets secondaires sont corrects, pas la mécanique interne.

Dmitriy Likhten
la source
2
Belle réponse, m'a donné la certitude que l'écriture de tests après le code peut toujours être utile et possible.
pixelastic
2
Un parfait exemple récent. J'avais une fonction très simple. Passez-le vrai, il fait une chose, faux il en fait une autre. TRÈS SIMPLE. Avait comme 4 tests vérifiant que la fonction fait ce qu'elle a l'intention de faire. Je change un peu le comportement. Exécutez des tests, POW un problème. Le plus drôle, c'est qu'en utilisant l'application, le problème ne se manifeste pas, ce n'est que dans un cas complexe qu'il le fait. Le cas de test l'a trouvé et je me suis sauvé des heures de maux de tête.
Dmitriy Likhten
"Les tests devraient tester l'intention." Je pense que cela résume, que vous devez passer par les utilisations prévues du code et vous assurer que le code peut les accueillir. Il indique également la portée de ce que le test doit réellement tester et l'idée que, lorsque vous effectuez un changement de code, vous ne pouvez pas considérer en bas de la ligne comment ce changement affecte toutes les utilisations prescrites du code - le test se défend contre un changement qui ne satisfait pas tous les cas d'utilisation prévus.
Greenstick
18

Il convient de noter que le réajustement des tests unitaires dans le code existant est beaucoup plus difficile que de conduire la création de ce code avec des tests en premier lieu. C'est l'une des grandes questions dans le traitement des applications héritées ... comment effectuer des tests unitaires? Cela a été posé plusieurs fois auparavant (vous pouvez donc être fermé en tant que question dupe), et les gens se retrouvent généralement ici:

Déplacement du code existant vers Test Driven Development

J'appuie la recommandation du livre de la réponse acceptée, mais au-delà, il y a plus d'informations liées dans les réponses.

David
la source
3
Si vous écrivez des tests en premier ou en deuxième, c'est très bien, mais lorsque vous écrivez des tests, vous vous assurez que votre code est testable afin de pouvoir écrire des tests. Vous vous retrouvez à penser "comment puis-je tester cela" souvent qui en soi entraîne une meilleure écriture du code. La mise à niveau des cas de test est toujours un gros non. Très dur. Ce n'est pas un problème de temps, c'est un problème de quantité et de testabilité. Je ne peux pas contacter mon patron en ce moment et dire que je veux écrire des cas de test pour nos plus de mille tables et utilisations, c'est trop maintenant, cela me prendrait un an, et certaines des logiques / décisions sont oubliées. Alors ne
tardez
2
Vraisemblablement, la réponse acceptée a changé. Il y a une réponse de Linx qui recommande L'art des tests unitaires par Roy Osherove, manning.com/osherove
thelem
15

N'écrivez pas de tests pour obtenir une couverture complète de votre code. Écrivez des tests qui garantissent vos exigences. Vous pouvez découvrir des chemins de code inutiles. Inversement, s'ils sont nécessaires, ils sont là pour remplir une sorte d'exigence; trouver ce que c'est et tester l'exigence (pas le chemin).

Gardez vos tests petits: un test par exigence.

Plus tard, lorsque vous devez apporter une modification (ou écrire un nouveau code), essayez d'abord d'écrire un test. Juste un. Vous aurez ensuite franchi la première étape du développement piloté par les tests.

Jon Reid
la source
Merci, il est logique de n'avoir que de petits tests pour de petites exigences, un à la fois. Leçon apprise.
pixelastic
13

Le test unitaire concerne la sortie que vous obtenez d'une fonction / méthode / application. Peu importe comment le résultat est produit, il importe juste qu'il soit correct. Par conséquent, votre approche du comptage des appels aux méthodes internes et autres est fausse. Ce que j'ai tendance à faire, c'est de m'asseoir et d'écrire ce qu'une méthode devrait retourner étant donné certaines valeurs d'entrée ou un certain environnement, puis d'écrire un test qui compare la valeur réelle retournée avec ce que j'ai trouvé.

fresskoma
la source
Merci ! J'avais le sentiment de me tromper, mais avoir quelqu'un me le dit, c'est mieux.
pixelastic
8

Essayez d'écrire un test unitaire avant d'écrire la méthode qu'il va tester.

Cela vous forcera certainement à penser un peu différemment à la façon dont les choses se font. Vous n'aurez aucune idée du fonctionnement de la méthode, de ce qu'elle est censée faire.

Vous devez toujours tester les résultats de la méthode, pas comment la méthode obtient ces résultats.

Justin Niessner
la source
Oui, j'adorerais pouvoir le faire, sauf que les méthodes sont déjà écrites. Je veux juste les tester. J'écrirai des tests avant les méthodes à l'avenir, tho.
pixelastic
2
@pixelastic prétend que les méthodes n'ont pas été écrites?
committedandroider
4

les tests sont censés améliorer la maintenabilité. Si vous changez une méthode et un test, cela peut être une bonne chose. D'un autre côté, si vous regardez votre méthode comme une boîte noire, peu importe ce qui se trouve à l'intérieur de la méthode. Le fait est que vous devez vous moquer des choses pour certains tests, et dans ces cas, vous ne pouvez vraiment pas traiter la méthode comme une boîte noire. La seule chose que vous pouvez faire est d'écrire un test d'intégration - vous chargez une instance entièrement instanciée du service testé et faites-le faire comme il le ferait dans votre application. Ensuite, vous pouvez le traiter comme une boîte noire.

When I'm writing tests for a method, I have the feeling of rewriting a second time what I          
already wrote in the method itself.
My tests just seems so tightly bound to the method (testing all codepath, expecting some    
inner methods to be called a number of times, with certain arguments), that it seems that
if I ever refactor the method, the tests will fail even if the final behavior of the   
method did not change.

C'est parce que vous écrivez vos tests après avoir écrit votre code. Si vous le faisiez dans l'autre sens (vous avez d'abord écrit les tests), ce ne serait pas le cas.

hvgotcodes
la source
Merci pour l'exemple de la boîte noire, je ne l'ai pas pensé de cette façon. Je souhaite avoir découvert les tests unitaires plus tôt, mais malheureusement, ce n'est pas le cas et je suis coincé avec une application héritée pour ajouter des tests. N'y a-t-il pas moyen d'ajouter des tests à un projet existant sans qu'ils se sentent cassés?
pixelastic
1
L'écriture des tests après est différente de celle des tests précédents, vous êtes donc coincé avec cela. cependant, vous pouvez configurer les tests de manière à ce qu'ils échouent en premier, puis les faire passer en mettant votre classe sous test .... faites quelque chose comme ça, en mettant votre instance sous test après l'échec initial du test. Même chose avec les simulacres - au début, le simulacre n'a aucune attente, et échouera parce que la méthode testée fera quelque chose avec le simulacre, puis réussira le test. Je ne serais pas surpris si vous trouvez beaucoup de bugs de cette façon.
hvgotcodes
aussi, soyez vraiment précis avec vos attentes. N'affirmez pas simplement que le test renvoie un objet, testez que l'objet a différentes valeurs dessus. Testez que lorsqu'une valeur est censée être nulle, c'est le cas. Vous pouvez également le décomposer un peu en faisant une refactorisation que vous vouliez faire, après avoir ajouté des tests.
hvgotcodes