L'écriture manuelle des tests unitaires est-elle une preuve d'exemple?

9

Nous savons que l'écriture de tests JUnit illustre un chemin particulier à travers votre code.

Un de mes associés a commenté:

L'écriture manuelle de tests unitaires est une preuve par exemple .

Il venait de l'arrière-plan de Haskell qui a des outils comme Quickcheck et la capacité de raisonner sur le comportement du programme avec les types .

Son implication était qu'il existe de nombreuses autres combinaisons d'entrées non testées par cette méthode pour lesquelles votre code n'est pas testé.

Ma question est la suivante: les tests unitaires écrits manuellement sont - ils des preuves par exemple?

oeil de faucon
la source
3
Non, ne pas écrire / utiliser de tests. Prétendre que vos tests unitaires sont la preuve qu'il n'y a rien de mal avec le programme est la preuve par l'exemple (une généralisation inappropriée). Les tests ne visent pas à prouver mathématiquement l'exactitude du code - les tests sont, par nature, des vérifications expérimentales. C'est un filet de sécurité qui vous aide à gagner la confiance en vous disant quelque chose sur le code. Mais vous êtes celui qui doit choisir une bonne stratégie pour sonder le code, et c'est vous qui devez interpréter la signification de ces données.
Filip Milovanović

Réponses:

10

Si vous choisissez au hasard des entrées pour les tests, je suppose qu'il est possible que vous exerciez une erreur logique de preuve par exemple.

Mais de bons tests unitaires ne font jamais cela. Au lieu de cela, ils traitent des plages et des cas de bord.

Par exemple, si vous deviez écrire des tests unitaires pour une fonction de valeur absolue qui accepte un entier en entrée, vous n'auriez pas besoin de tester toutes les valeurs d'entrée possibles pour prouver que le code fonctionne. Pour obtenir un test complet, vous n'auriez besoin que de cinq valeurs: -1, 0, 1 et les valeurs max et min pour l'entier d'entrée.

Ces cinq valeurs testent chaque plage possible et chaque cas de bord de la fonction. Vous n'avez pas besoin de tester toutes les autres valeurs d'entrée possibles (c'est-à-dire chaque nombre que le type entier peut représenter) pour obtenir un niveau de confiance élevé que la fonction fonctionne pour toutes les valeurs d'entrée.

Robert Harvey
la source
11
Un testeur de code entre dans un bar et commande une bière. 5 bières. -1 bières, MAX_VALUE bières, un poulet. un nul.
Neil
2
Les "5 valeurs" sont un pur non-sens. Considérez une fonction triviale comme int foo(int x) { return 1234/(x - 100); }. Notez également que (en fonction de ce que vous testez), vous devrez peut-être vous assurer qu'une entrée non valide ("hors plage") renvoie des résultats corrects (par exemple, que `` find_thing (thing) `renvoie correctement une sorte d'état" non trouvé "). si la chose n'a pas été trouvée).
Brendan
3
@Brendan: Il n'y a rien de significatif à ce que ce soit cinq valeurs; il se trouve que ce sont cinq valeurs dans mon exemple. Votre exemple comporte un nombre différent de tests car vous testez une fonction différente. Je ne dis pas que chaque fonction nécessite exactement cinq tests; vous en avez déduit la lecture de ma réponse.
Robert Harvey
1
Les bibliothèques de tests génératifs sont généralement plus performantes que vous pour tester les cas limites. Si, par exemple, que vous utilisiez des flotteurs au lieu d'entiers, votre bibliothèque vérifierait aussi -Inf, Inf, NaN, 1e-100, -1e-100, -0, 2e200... je préfère ne pas avoir à faire les tout manuellement.
Hovercouch
@Hovercouch: Si vous en connaissez un bon, j'aimerais en entendre parler. Le meilleur que j'ai vu était Pex; c'était incroyablement instable, cependant. Rappelez-vous, nous parlons ici de fonctions relativement simples. Les choses deviennent plus difficiles lorsque vous traitez avec des choses comme la logique métier réelle.
Robert Harvey
8

Tout test de logiciel est comme "Proof By Example", pas seulement les tests unitaires utilisant un outil comme JUnit. Et ce n'est pas une nouvelle sagesse, il y a une citation de Dijkstra de 1960, qui dit essentiellement la même chose:

"Les tests montrent la présence, pas l'absence de bugs"

(il suffit de remplacer les mots "montre" par "preuves"). Cependant, cela est également vrai pour les outils qui génèrent des données de test aléatoires. Le nombre d'entrées possibles pour une fonction du monde réel est généralement plus élevé par ordre de grandeur que le nombre de cas de test que l'on peut produire et vérifier par rapport à un résultat attendu à l'ère de l'univers, indépendamment de la méthode de génération de ces cas, donc même si l'on utilise un outil générateur pour produire beaucoup de données de test, il n'y a aucune garantie de ne pas manquer le seul cas de test qui aurait pu détecter un certain bug.

Les tests aléatoires peuvent parfois révéler un bogue qui a été ignoré par les cas de test créés manuellement. Mais en général, il est plus efficace de créer soigneusement des tests pour la fonction à tester et de s'assurer que l'on obtient un code complet et une couverture de branche avec le moins de cas de test possible. Parfois, il est possible de combiner des tests générés manuellement et aléatoirement. De plus, lors de l'utilisation de tests aléatoires, il faut veiller à obtenir des résultats reproductibles.

Les tests créés manuellement ne sont donc en aucun cas pires que les tests générés de manière aléatoire, souvent bien au contraire.

Doc Brown
la source
1
Toute suite de tests pratique utilisant la vérification aléatoire aura également des tests unitaires. (Techniquement, les tests unitaires ne sont qu'un cas dégénéré de tests aléatoires.) Votre formulation suggère que les tests randomisés sont difficiles à réaliser, ou qu'il est difficile de combiner les tests randomisés et les tests unitaires. Ce n'est généralement pas le cas. À mon avis, l'un des plus grands avantages des tests randomisés est qu'il encourage fortement l'écriture de tests en tant que propriétés du code qui sont censées être toujours conservées. Je préférerais de loin que ces propriétés soient explicitement déclarées (et vérifiées!) Plutôt que de leur déduire des tests ponctuels.
Derek Elkins a quitté le SE
@DerekElkins: "difficile" est à mon humble avis le mauvais terme. Les tests aléatoires nécessitent un certain effort, et c'est un effort qui réduit le temps disponible pour les tests d'artisanat (et si vous avez des gens qui suivent simplement des slogans comme celui mentionné dans la question, ils ne feront probablement pas d'artisanat du tout). Le simple fait de jeter beaucoup de données de test aléatoires sur un morceau de code ne représente que la moitié du travail, il faut également produire les résultats attendus pour chacune de ces entrées de test. Pour certains scénarios, cela peut être fait automatiquement. Pour les autres, non.
Doc Brown
Bien qu'il y ait certainement des moments où une réflexion est nécessaire pour choisir une bonne distribution, ce n'est généralement pas un gros problème. Votre commentaire suggère que vous pensez à cela dans le mauvais sens. Les propriétés que vous écrivez pour la vérification aléatoire sont les mêmes que celles que vous écririez pour la vérification de modèle ou pour les preuves formelles. En effet, ils peuvent être et ont été utilisés pour toutes ces choses en même temps. Il n'y a pas non plus de "résultats attendus" à produire. Au lieu de cela, vous indiquez simplement une propriété qui doit toujours être conservée. Quelques exemples: 1) pousser quelque chose sur une pile et ...
Derek Elkins a quitté le SE
... alors popping devrait être la même chose que ne rien faire; 2) tout client avec un solde supérieur à 10 000 $ devrait bénéficier du taux d'intérêt de solde élevé et seulement alors; 3) la position du sprite est toujours dans la zone de délimitation de l'écran. Certaines propriétés peuvent bien correspondre à des tests ponctuels, par exemple "lorsque le solde est de 0 $, donnez l'avertissement de solde nul". Les propriétés sont des spécifications partielles avec l'idéal d'obtenir une spécification totale. Ayant des difficultés à imaginer ces propriétés signifie que vous n'êtes pas clair sur ce qu'est la spécification et signifie souvent que vous auriez des difficultés à imaginer de bons tests unitaires.
Derek Elkins a quitté le SE
0

L'écriture manuelle de tests est une "preuve par l'exemple". Mais QuickCheck l'est aussi, et dans une mesure limitée, les systèmes de type. Tout ce qui n'est pas une vérification formelle directe sera limité dans ce qu'il vous dit sur votre code. Au lieu de cela, vous devez penser en termes de mérite relatif des approches.

Les tests génératifs, comme QuickCheck, sont vraiment bons pour balayer un large espace d'entrées. C'est aussi beaucoup mieux pour s'attaquer aux cas marginaux que les tests manuels: les bibliothèques de tests génératifs seront plus expérimentées que vous dans ce domaine. D'un autre côté, ils ne vous parlent que des invariants, pas des sorties spécifiques. Donc , pour valider votre programme obtient les résultats corrects, vous avez encore besoin de quelques tests manuels pour vérifier que, en fait, foo(bar) = baz.

Hovercouch
la source