3
votes

Quoi tester unitaire

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:

  • AppendRepeats_ZeroRepeats_DontAppend
  • AppendRepeats_NegativeRepeats_DontAppend
  • AppendRepeats_PositiveRepeats_Append
  • AppendRepeats_NullStrBZeroRepeats_DontAppend
  • AppendRepeats_NullStrBNegativeRepeats_DontAppend
  • AppendRepeats_NullStrBPositiveRepeats_Append
  • AppendRepeats_EmptyStrBZeroRepeats_DontAppend
  • AppendRepeats_EmptyStrBNegativeRepeats_DontAppend
  • AppendRepeats_EmptyStrBPositiveRepeats_Append
  • etc. etc. strB peut être nul, vide ou avoir une valeur. c peut être nul ou avoir une valeur les répétitions peuvent être négatives, positives ou nulles

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.


7 commentaires

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.


4 Réponses :


2
votes

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.


3 commentaires

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.



1
votes

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.


0 commentaires

3
votes

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:

  • Si vous adoptez le développement piloté par les tests, dans lequel vous n'écrivez jamais de code autre que de test, sauf si vous avez d'abord écrit un test unitaire qui échoue, cela vous guidera dans le nombre de tests que vous écrivez. Avec une certaine expérience en TDD, vous aurez une idée de cela, donc même si vous avez besoin d'écrire des tests unitaires pour un ancien code qui n'était pas TDD, vous pourrez écrire des tests comme si c'était le cas.
  • Si une classe a trop de tests unitaires, c'est une indication que la classe fait trop de choses. «Trop» est cependant difficile à quantifier. Mais quand vous vous sentez trop nombreux, essayez de diviser la classe en plusieurs classes, chacune avec moins de responsabilités.
  • La moquerie est fondamentale pour les tests unitaires. Sans se moquer des collaborateurs, vous testez plus que l '«unité». Alors apprenez à utiliser un framework moqueur.
  • La vérification des valeurs nulles et le test de ces vérifications peuvent générer beaucoup de code. Si vous adoptez un style dans lequel vous ne produisez jamais de valeur nulle, votre code n'a jamais besoin de gérer une valeur nulle et il n'est pas nécessaire de tester ce qui se passe dans cette circonstance.
    • Il existe des exceptions à cela, par exemple si vous fournissez du code de bibliothèque et que vous souhaitez donner à l'appelant des erreurs de paramètres invalides amicales
  • Pour certaines méthodes, les tests de propriétés peuvent être un moyen viable de frapper votre code avec de nombreux tests. @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»

0 commentaires

2
votes

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.


0 commentaires