Je ne sais pas trop combien je devrais consacrer aux tests unitaires.
Disons que j'ai une fonction simple comme:
strB = "hello" c = "h" repeats = 5 // result strB = "hellohhhhh"
[Cette fonction ajoutera char c se répète un certain nombre de fois à strB. par exemple :
appendRepeats(StringBuilder strB, char c, int repeats)
Pour les tests unitaires de cette fonction, je pense qu'il y a déjà tellement de possibilités:
Cela semble déjà 3 * 2 * 3 = 18 méthodes de test. Cela pourrait être beaucoup plus sur d'autres fonctions si ces fonctions doivent également tester des caractères spéciaux, Integer.MIN_VALUE, Integer.MAX_VALUE, etc. etc. Quelle devrait être ma ligne d'arrêt? Dois-je supposer aux fins de mon propre programme: strB ne peut être que vide ou avoir une valeur c a de la valeur les répétitions ne peuvent être que vides ou positives
Désolé pour la peine. Je suis vraiment confus à quel point je devrais être paranoïaque avec les tests unitaires en général. Dois-je rester dans les limites de mes hypothèses ou est-ce une mauvaise pratique et devrais-je avoir une méthode pour chaque cas potentiel auquel cas, le nombre de méthodes de test unitaire évoluerait de manière exponentielle assez rapide.
4 Réponses :
La règle générale est généralement que chaque 'fourchette' de votre code doit avoir un test, ce qui signifie que vous devez couvrir tous les cas de bord possibles.
Par exemple, si vous avez le code suivant:
if (x != null) { if (x.length > 100) { // do something } else { // do something else } } else { // do something completely else }
Vous devriez avoir trois cas de test: un pour null, un pour une valeur inférieure à 100 et un pour plus. C'est si vous êtes strict et que vous voulez être couvert à 100%.
Que ce soit des tests différents ou paramétrés est moins important, c'est plus une question de style et vous pouvez aller dans les deux sens. Je pense que le plus important est de couvrir tous les cas.
Cela semble suggérer que la structure de vos tests devrait être basée sur l ' implémentation de la méthode que vous testez, plutôt que sur les attentes de son comportement, indépendamment des détails de l'implémentation.
@khelwood - si vous parlez de tests unitaires, oui, dans la plupart des cas, c'est le cas. Il est également dicté par la façon dont vous le divisez en classes et en fonctions. C'est un cas différent pour les tests système / d'intégration, où l'API définit les tests
Je ne pense pas que ce conseil soit nécessairement basé sur l'implémentation, vous aurez ce genre de points de décision si vous écrivez les tests à partir d'une spécification ou si vous le codez avec TDD. la différence est que si vous avez une spécification, elle sera probablement incomplète.
Tout d'abord, utilisez un outil de couverture de code. Cela vous montrera quelles lignes de votre code sont exécutées par vos tests. Les IDE ont des plugins pour les outils de couverture de code afin que vous puissiez exécuter un test et voir quelles lignes ont été exécutées. Essayez de couvrir chaque ligne, ce qui peut être difficile dans certains cas, mais pour ce type d'utilitaire, c'est très faisable.
L'utilisation de l'outil de couverture de code fait ressortir les cas de bord découverts. Pour les tests qui sont plus difficiles à implémenter, la couverture de code vous montre les lignes que votre test a exécutées, donc s'il y a une erreur dans votre test, vous pouvez voir jusqu'où elle a abouti.
Ensuite, comprenez qu'aucun test ne couvre tout . Il y aura toujours des valeurs que vous ne testerez pas. Choisissez donc des entrées représentatives qui vous intéressent et évitez celles qui semblent redondantes. Par exemple, la transmission d'un StringBuilder vide vous tient-elle vraiment à cœur? Cela n'affecte pas le comportement du code. Il existe des valeurs spéciales qui peuvent poser un problème, comme null. Si vous testez une recherche binaire, vous voudrez couvrir le cas où le tableau est vraiment grand, pour voir si le calcul du point médian déborde. Recherchez les types de cas qui comptent.
Si vous validez d'avance et supprimez les valeurs gênantes, vous n'avez pas à faire autant de tests de travail. Un test pour null StringBuilder passé pour vérifier que vous lancez IllegalArgumentException, un test pour une valeur de répétition négative pour vérifier que vous lancez quelque chose pour cela.
Enfin, les tests sont pour les développeurs. Faites ce qui vous est utile.
Il n'y a pas de bonne réponse, et c'est une question d'opinion et de sentiments personnels.
Cependant, je pense que certaines choses sont universelles:
@Theory
de jUnit en est une implémentation. Il vous permet de tester des assertions telles que « plus (x, y
) renvoie un nombre positif pour tout x positif et y positif» L'ensemble de cas de test que vous avez développé est le résultat d'une approche de conception de test en boîte noire, en fait, ils semblent avoir appliqué la méthode de l'arborescence de classification. Bien qu'il soit parfaitement normal de prendre temporairement une perspective de boîte noire lors de tests unitaires, vous limiter aux tests de boîte noire seulement peut avoir des effets indésirables: Premièrement, comme vous l'avez observé, vous pouvez vous retrouver avec le produit cartésien de tous scénarios possibles pour chacune des entrées, deuxièmement, vous ne trouverez probablement toujours pas de bogues spécifiques à l'implémentation choisie.
En prenant (aussi) une perspective de boîte en verre (aka boîte blanche), vous pouvez éviter de créer des tests inutiles: sachant que votre code en tant que première étape gère le cas particulier où le nombre de répétitions est négatif signifie que vous ne le faites pas. Il faut multiplier ce scénario par tous les autres. Certes, cela signifie que vous utilisez votre connaissance des détails d'implémentation: si vous deviez par la suite modifier votre code de telle sorte que la vérification des répétitions négatives se fasse à plusieurs endroits, vous feriez mieux d'ajuster également votre suite de tests.
Puisqu'il semble y avoir une préoccupation largement répandue concernant le test des détails de l'implémentation: le test unitaire consiste à tester l'implémentation. Différentes implémentations ont différents bogues potentiels. Si vous n'utilisez pas les tests unitaires pour trouver ces bogues, alors tout autre niveau de test (intégration, sous-système, système) est nettement moins adapté pour les trouver systématiquement - et dans un projet plus important, vous ne voulez pas que les bogues de niveau d'implémentation s'échappent. les phases de développement ultérieures ou même sur le terrain. En passant, l'analyse de la couverture implique que vous adoptiez une perspective de boîte en verre, et TDD fait de même.
Il est cependant correct qu'une suite de tests ou des tests individuels ne doivent pas inutilement dépendre des détails de l'implémentation - mais c'est une déclaration différente de celle de ne pas dépendre du tout des détails de l'implémentation. Une approche plausible consiste donc à avoir un ensemble de tests qui ont du sens du point de vue de la boîte noire, ainsi que des tests destinés à détecter les bogues spécifiques à l'implémentation. Ce dernier doit être ajusté lorsque vous modifiez votre code, mais l'effort peut être réduit par divers moyens, par ex. en utilisant des méthodes d'assistance de test, etc.
Dans votre cas, prendre une perspective de boîte de verre réduirait probablement le nombre de tests avec des répétitions négatives à un, également les cas de caractères null, peut-être aussi les cas de NullStrB (en supposant que vous gérez cela tôt en remplaçant le null par un vide string), et ainsi de suite.
Vous n'avez pas besoin de 18 méthodes différentes si vous utilisez des tests paramétrés.
Aussi peut-être que certains paramètres sont plus importants que d'autres, si StringBuilder est nul, je suppose que la méthode ne peut rien faire, donc les autres valeurs de paramètre ne sont pas pertinentes si
strB
est nul et donc un seul test est nécessaire pour ce. Vous devez également utiliser un outil pour la couverture de test et avoir une idée du pourcentage à couvrir, ce n'est pas nécessairement 100%Les tests basés sur les propriétés peuvent être intéressants dans ce cas
pourquoi tous les tests avec null str, ne lanceriez-vous pas simplement IllegalArgumentException?
@NathanHughes Vous avez raison. Je n'ai pas besoin de tester autre chose que le cas nul pour StringBuilder. Mais ce serait toujours un test de 12 unités pour une méthode simple.
@Khelwood woah. Merci. Donc, je suppose une méthode de test unitaire pour tester tous les cas par fonction?
@AccCreate Bien sûr, si vous pouvez couvrir tous les cas dont vous avez besoin avec un seul test paramétré, tant mieux.