Test: déterministe ou non déterministe?

16

Vaut-il mieux avoir un

  • Suite de tests déterministes, qui se traduit par la réussite des mêmes tests
  • Suite de tests non déterministes, qui peut éventuellement couvrir plus de cas

?

Exemple: vous écrivez une suite de tests pour tester les fonctionnalités du contrôleur dans une application MVC. Le contrôleur requiert des données d'application d'une base de données en entrée pendant le test. Il existe deux options pour ce faire:

  • Vous codez en dur la ou les lignes de la base de données de test qui sont sélectionnées en entrée (par exemple, la 10e et la 412e ligne)
  • Vous utilisez un générateur de nombres aléatoires pour sélectionner de manière pseudo-aléatoire les données de la base de données (deux lignes sélectionnées par un générateur de nombres aléatoires)

La première est déterministe: chaque exécution du test pour la même révision de code devrait produire le même résultat. La seconde n'est pas déterministe: chaque exécution de la suite de tests a la possibilité de produire un résultat différent. Les données sélectionnées au hasard pourraient cependant être une meilleure représentation des cas de bord de données. Cela pourrait mieux simuler un utilisateur alimentant nos contrôleurs avec des données imprévisibles?

Quelles sont les raisons de choisir l'un plutôt que l'autre?

DCKing
la source
5
Ce test échoue parfois, parfois. martinfowler.com/articles/nonDeterminism.html
Merci pour ce lien. Avec cet article à l'esprit, j'ai senti que je devais clarifier que le non-déterminisme signifie dans le contexte de cette suite de tests. Étant donné que les données sont sélectionnées au hasard dans une base de données, toutes les données transmises au contrôleur sont des données valides par défaut. Cela signifie que les faux négatifs n'existent pas dans la suite de tests en ce qui concerne le non déterminisme. D'une certaine manière, ce caractère aléatoire simule un utilisateur sélectionnant des données «au hasard» pour les utiliser dans un contrôleur. Ce n'est pas nécessairement le même non-déterminisme dont parle l'article, non?
DCKing du
10
@DCKing: Considérez ce qui se passe si votre test échoue. D'accord, vous avez un bug. Euh, maintenant quoi? Exécutez-le à nouveau en mode débogage! Là où ça réussit! Comme il le fait les cent prochaines fois que vous l'exécutez, puis vous annulez le problème en tant que coup de rayon cosmique. La non-détermination dans les tests semble absolument irréalisable. Si vous ressentez le besoin de couvrir plus de terrain dans vos cas de test, couvrez plus de terrain. Initialisez votre RNG avec une valeur de départ définie et exécutez le "test" quelques centaines de fois avec des valeurs aléatoires cohérentes.
Phoshi
1
(enfin arrivé à une machine où je pouvais correctement rechercher sur Twitter - le " Ce test échoue parfois " vient de #FiveWordTechHorrors sur Twitter - voulait le créditer correctement)

Réponses:

30

Lorsque chaque exécution de la suite de tests vous donne la possibilité de produire un résultat différent, le test est presque sans valeur - lorsque la suite vous montre un bogue, vous avez de grandes chances de ne pas pouvoir le reproduire, et lorsque vous essayez de corriger le bug, vous ne pouvez pas vérifier si votre correctif fonctionne.

Donc, lorsque vous pensez que vous devez utiliser une sorte de générateur de nombres aléatoires pour générer vos données de test, assurez-vous de toujours initialiser le générateur avec la même graine, ou de conserver vos données de test aléatoires dans un fichier avant de les alimenter dans votre test, de sorte que vous pouvez réexécuter le test avec exactement les mêmes données de l'exécution précédente. De cette façon, vous pouvez transformer tout test non déterministe en test déterministe.

EDIT: L'utilisation d'un générateur de nombres aléatoires pour sélectionner certaines données de test est à mon humble avis parfois un signe d'être trop paresseux pour choisir de bonnes données de test. Au lieu de lancer 100 000 valeurs de test choisies au hasard et espérons que ce sera suffisant pour découvrir tous les bugs graves par hasard, mieux utiliser votre cerveau, choisir 10 à 20 cas "intéressants" et les utiliser pour la suite de tests. Cela se traduira non seulement par une meilleure qualité de vos tests, mais également par des performances beaucoup plus élevées de la suite.

Doc Brown
la source
Merci pour votre réponse. Quelle est votre opinion sur le commentaire que j'ai fait à ma question?
DCKing du
1
@DCKing: si vous pensez vraiment qu'un générateur aléatoire sera meilleur dans la sélection de bons cas de test que vous (ce dont je doute), utilisez-le une fois pour trouver des combinaisons de données de test là où votre programme échoue, et mettez ces combinaisons dans la partie "codée en dur" de votre suite de tests.
Doc Brown
Merci encore. Mise à jour de ma réponse afin qu'elle ne semble pas s'appliquer uniquement aux applications MVC.
DCKing
1
Dans certains contextes d'interface utilisateur (par exemple, les jeux prenant l'entrée du contrôleur), avoir des programmes de test qui génèrent une entrée de clé aléatoire peut être utile pour les tests de stress. Ils peuvent découvrir des défauts qui sont difficiles à trouver avec une entrée délibérée.
Gort le robot
@StevenBurnap: eh bien, d'après ma compréhension de la question, je pense que l'OP avait en tête des tests de régression plus conventionnels. Bien sûr, je suis d'accord, les tests de résistance sont un cas spécial qui peut également dépendre du matériel et entraîner un comportement non déterministe même lorsque vous n'utilisez pas de générateur aléatoire. C'est quelque chose décrit dans l'article lié par MichaelT dans le premier commentaire sous la question. Et même dans les tests de résistance avec entrée aléatoire, on peut au moins essayer de rendre le comportement plus déterministe en utilisant une graine aléatoire définie.
Doc Brown
4

Les deux déterministes et non déterministes ont une place

Je les diviserais comme suit:

Tests unitaires.

Ceux-ci devraient avoir des tests déterministes et reproductibles avec les mêmes données exactes à chaque fois. Les tests unitaires accompagnent des sections de code spécifiques et isolées et doivent les tester de manière déterministe.

Tests de stress fonctionnel et d'entrée.

Ceux-ci peuvent utiliser l'approche non déterministe avec les mises en garde suivantes:

  • ce fait est clairement délimité et appelé
  • les valeurs aléatoires sélectionnées sont enregistrées et peuvent être réessayées manuellement
Michael Durrant
la source
3

Tous les deux.

Les tests déterministes et non déterministes ont différents cas d'utilisation et différentes valeurs pour votre suite. En règle générale , les tests non déterministes ne peuvent pas fournir la même précision que les tests déterministes, qui sont progressivement devenus des «tests non déterministes qui n'ont aucune valeur». C'est faux. Ils peuvent être moins précis, mais ils peuvent également être beaucoup plus larges, ce qui a ses propres avantages.

Prenons un exemple: vous écrivez une fonction qui trie une liste d'entiers. Quels seraient certains des tests unitaires déterministes que vous jugeriez utiles?

  • Une liste vide
  • Une liste avec un seul élément
  • Une liste avec tous le même élément
  • Une liste avec plusieurs éléments uniques
  • Une liste avec plusieurs éléments, dont certains sont des doublons
  • Une liste NaN, INT_MINetINT_MAX
  • Une liste déjà partiellement triée
  • Une liste avec 10 000 000 d'éléments

Et ce n'est qu'une fonction de tri! Bien sûr, vous pourriez faire valoir que certains d'entre eux ne sont pas nécessaires, ou que certains d'entre eux peuvent être exclus par un raisonnement informel. Mais nous sommes des ingénieurs et nous avons vu un raisonnement informel exploser dans notre visage. Nous savons que nous ne sommes pas assez intelligents pour comprendre complètement les systèmes que nous avons construits ou pour garder complètement la complexité dans nos têtes. C'est pourquoi nous écrivons des tests en premier lieu. L'ajout de tests non déterministes signifie simplement que nous ne sommes pas nécessairement assez intelligents pour connaître tous les bons tests a priori. En lançant des données semi-aléatoires dans votre fonction, vous êtes beaucoup plus susceptible de trouver un cas de bord que vous avez manqué.

Bien sûr, cela n'exclut pas non plus les tests déterministes. Les tests non déterministes aident à trouver des bogues dans de vastes pans du programme. Une fois que vous avez trouvé les bogues, cependant, vous avez besoin d'un moyen reproductible pour montrer que vous les avez corrigés. Donc:

  • Utilisez des tests non déterministes pour trouver des bogues dans votre code.
  • Utilisez des tests déterministes pour vérifier les correctifs dans votre code.

Notez que cela signifie que de nombreux conseils solides sur les tests unitaires ne s'appliquent pas nécessairement aux tests non déterministes. Par exemple, qu'ils doivent être rapides. Les tests de propriétés de bas niveau doivent être rapides, mais un test non déterministe comme "simuler un utilisateur en cliquant au hasard sur les boutons de votre site Web et vous assurer de ne jamais obtenir d'erreur 500" devrait favoriser l'exhaustivité par rapport à la vitesse. Faites simplement exécuter un test comme celui-ci indépendamment de votre processus de génération afin qu'il ne ralentisse pas le développement. Par exemple, exécutez-le sur sa propre boîte de transfert privée.

Hovercouch
la source
-1

Vous ne voulez pas vraiment déterministe vs non déterministe.

Ce que vous voudrez peut-être, c'est «toujours le même» vs «pas toujours le même».

Par exemple, vous pouvez avoir un numéro de build qui augmente avec chaque build, et lorsque vous voulez des nombres aléatoires, vous initialisez un générateur de nombres aléatoires avec le numéro de build comme graine. Donc, à chaque build, vous faites vos tests avec des valeurs différentes, vous donnant plus de chances de trouver des bugs.

Mais une fois qu'un bogue a été trouvé, il vous suffit d'exécuter le test avec le même numéro de build et il est reproductible.

gnasher729
la source
1
Ou si vous n'avez pas de numéro de build à utiliser, placez la valeur initiale de la valeur de départ dans la sortie de l'exécution de test, afin que vous puissiez à nouveau réexécuter les tests avec la même valeur de départ.
RemcoGerlich