Je travaille fréquemment avec des programmes très numériques/mathématiques, où le résultat exact d'une fonction est difficile à prévoir à l'avance.
En essayant d'appliquer TDD avec ce type de code, je trouve souvent que l'écriture du code sous test est beaucoup plus facile que l'écriture de tests unitaires pour ce code, car la seule façon que je connaisse pour trouver le résultat attendu est d'appliquer l'algorithme lui-même (que ce soit dans mon tête, sur papier ou par ordinateur). Cela ne me semble pas correct, car j'utilise efficacement le code sous test pour vérifier mes tests unitaires, plutôt que l'inverse.
Existe-t-il des techniques connues pour écrire des tests unitaires et appliquer TDD lorsque le résultat du code testé est difficile à prévoir?
Un exemple (réel) de code avec des résultats difficiles à prévoir:
Une fonction weightedTasksOnTime
qui, compte tenu de la quantité de travail effectuée par jour workPerDay
dans la plage (0, 24], l'heure actuelle initialTime
> 0 et une liste de tâches taskArray
; chacun avec un temps pour terminer la propriété time
> 0, la date d'échéance due
et la valeur d'importance importance
; renvoie une valeur normalisée dans la plage [0, 1] représentant l'importance des tâches qui peuvent être achevées avant leur date due
si chaque tâche est terminée dans l'ordre donné par taskArray
, en commençant par initialTime
.
L'algorithme pour implémenter cette fonction est relativement simple: itérer sur les tâches dans taskArray
. Pour chaque tâche, ajoutez time
à initialTime
. Si la nouvelle heure <due
, ajoutez importance
à un accumulateur. Le temps est ajusté par workPerDay inverse. Avant de retourner l'accumulateur, divisez par la somme des importances de tâche pour normaliser.
function weightedTasksOnTime(workPerDay, initialTime, taskArray) {
let simulatedTime = initialTime
let accumulator = 0;
for (task in taskArray) {
simulatedTime += task.time * (24 / workPerDay)
if (simulatedTime < task.due) {
accumulator += task.importance
}
}
return accumulator / totalImportance(taskArray)
}
Je crois que le problème ci-dessus peut être simplifié, tout en conservant son cœur, en supprimant workPerDay
et l'exigence de normalisation, pour donner:
function weightedTasksOnTime(initialTime, taskArray) {
let simulatedTime = initialTime
let accumulator = 0;
for (task in taskArray) {
simulatedTime += task.time
if (simulatedTime < task.due) {
accumulator += task.importance
}
}
return accumulator
}
Cette question traite des situations où le code testé n'est pas une réimplémentation d'un algorithme existant. Si le code est une réimplémentation, il a intrinsèquement des résultats faciles à prévoir, car les implémentations fiables existantes de l'algorithme agissent comme un test naturel Oracle.
Il y a deux choses que vous pouvez tester dans un code difficile à tester. Tout d'abord, les cas dégénérés. Que se passe-t-il si vous n'avez aucun élément dans votre tableau de tâches, ou seulement un, ou deux mais un est passé la date d'échéance, etc. Tout ce qui est plus simple que votre problème réel, mais toujours raisonnable à calculer manuellement.
Le deuxième est les contrôles de santé mentale. Ce sont les vérifications que vous faites lorsque vous ne savez pas si une réponse est droite, mais vous savez certainement si c'est mauvais. Ce sont des choses comme le temps doit avancer, les valeurs doivent être dans une fourchette raisonnable, les pourcentages doivent totaliser 100, etc.
Oui, ce n'est pas aussi bon qu'un test complet, mais vous seriez surpris de voir combien de fois vous vous trompez sur les vérifications de santé mentale et les cas dégénérés, ce qui révèle un problème dans votre algorithme complet.
J'avais l'habitude d'écrire des tests pour des logiciels scientifiques avec des sorties difficiles à prévoir. Nous avons beaucoup utilisé les relations métamorphiques. Il y a essentiellement des choses que vous savez sur le comportement de votre logiciel même si vous ne connaissez pas les sorties numériques exactes.
Un exemple possible pour votre cas: si vous diminuez la quantité de travail que vous pouvez faire chaque jour, la quantité totale de travail que vous pouvez faire restera au mieux la même, mais diminuera probablement. Exécutez donc la fonction pour un certain nombre de valeurs de workPerDay
et assurez-vous que la relation est vraie.
Les autres réponses ont de bonnes idées pour développer des tests pour Edge ou le cas d'erreur. Pour les autres, utiliser l'algorithme lui-même n'est pas idéal (évidemment) mais toujours utile.
Il détectera si l'algorithme (ou les données dont il dépend) a changé
Si le changement est un accident, vous pouvez annuler un commit. Si le changement était délibéré, vous devez revoir le test unitaire.
De la même manière que vous écrivez des tests unitaires pour tout autre type de code:
À moins que votre code n'implique un élément aléatoire ou qu'il ne soit pas déterministe (c'est-à-dire qu'il ne produira pas la même sortie avec la même entrée), il est testable à l'unité.
Évitez les effets secondaires ou les fonctions qui sont affectés par des forces extérieures. Les fonctions pures sont plus faciles à tester.
La réponse d'origine a été supprimée par souci de concision - vous pouvez la trouver dans l'historique des modifications.
PaintingInAir Pour le contexte: en tant qu'entrepreneur et universitaire, la plupart des algorithmes que je conçois ne sont demandés par personne d'autre que moi-même. L'exemple donné dans la question fait partie d'un optimiseur sans dérivé pour maximiser la qualité d'un ordre des tâches. En ce qui concerne la façon dont j'ai décrit la nécessité de la fonction d'exemple en interne: "J'ai besoin d'une fonction objective pour maximiser l'importance des tâches qui sont terminées à temps". Cependant, il semble toujours y avoir un grand écart entre cette demande et la mise en œuvre des tests unitaires.
Tout d'abord, un TL; DR pour éviter une réponse autrement longue:
Pensez-y de cette façon:
Un client entre dans McDonald's et demande un hamburger avec de la laitue, de la tomate et du savon pour les mains comme garniture. Cette commande est donnée au cuisinier, qui prépare le burger exactement comme demandé. Le client reçoit ce burger, le mange, puis se plaint au cuisinier que ce n'est pas un savoureux burger!Ce n'est pas la faute du cuisinier - il ne fait que ce que le client a explicitement demandé. Ce n'est pas le travail du cuisinier de vérifier si la commande demandée est vraiment savoureuse. Le cuisinier crée simplement ce que le client commande. Il est de la responsabilité du client de commander quelque chose qu'il trouve bon.
De même, ce n'est pas au développeur de remettre en question l'exactitude de l'algorithme. Leur seul travail consiste à implémenter l'algorithme comme demandé.
Les tests unitaires sont un outil de développeur. Il confirme que le burger correspond à la commande (avant de quitter la cuisine). Il n'essaie pas (et ne devrait pas) confirmer que le hamburger commandé est réellement savoureux.Même si vous êtes à la fois le client et le cuisinier, il existe toujours une distinction significative entre:
- Je n'ai pas préparé ce repas correctement, il n'était pas bon (= erreur de cuisinier). Un steak brûlé ne sera jamais bon, même si vous aimez le steak.
- J'ai bien préparé le repas, mais je n'aime pas ça (= erreur client). Si vous n'aimez pas le steak, vous n'aimerez jamais le manger, même si vous l'avez cuit à la perfection.
Le principal problème ici est que vous ne faites pas de séparation entre le client et le développeur (et l'analyste - bien que ce rôle puisse également être représenté par un développeur).
Vous devez faire la distinction entre tester le code et tester les exigences métier.
Par exemple, le client souhaite que cela fonctionne comme [this] . Cependant, le développeur comprend mal, et il écrit du code qui fait [que] .
Le développeur écrira donc des tests unitaires qui testent si [que] fonctionne comme prévu. S'il a correctement développé l'application, ses tests unitaires passeront même si l'application ne le fait pas [this] , qui le client attendait.
Si vous souhaitez tester les attentes du client (les exigences commerciales), cela doit être fait dans une étape distincte (et ultérieure).
Un workflow de développement simple pour vous montrer quand ces tests doivent être exécutés:
Vous vous demandez peut-être à quoi sert de faire deux tests distincts lorsque le client et le développeur sont une seule et même personne. Puisqu'il n'y a pas de "transfert" du développeur au client, les tests sont exécutés l'un après l'autre, mais ce sont toujours des étapes distinctes.
Si vous voulez tester si votre algorithme lui-même est correct, cela ne fait pas partie du travail du développeur. C'est la préoccupation du client, et le client testera cela en en utilisant l'application.
En tant qu'entrepreneur et universitaire, vous pourriez manquer ici une distinction importante, qui met en évidence les différentes responsabilités.
Test de propriété
Parfois, les fonctions mathématiques sont mieux servies par les "tests de propriétés" que par les tests unitaires traditionnels basés sur des exemples. Par exemple, imaginez que vous écrivez des tests unitaires pour quelque chose comme une fonction "multiplier" entière. Bien que la fonction elle-même puisse sembler très simple, si c'est le seul moyen de se multiplier, comment la tester à fond sans la logique de la fonction elle-même? Vous pouvez utiliser des tableaux géants avec les entrées/sorties attendues, mais cela est limité et sujet aux erreurs.
Dans ces cas, vous pouvez tester les propriétés connues de la fonction, au lieu de rechercher des résultats attendus spécifiques. Pour la multiplication, vous savez peut-être que la multiplication d'un nombre négatif et d'un nombre positif doit entraîner un nombre négatif, et que la multiplication de deux nombres négatifs doit entraîner un nombre positif, etc. tester les valeurs est un bon moyen de tester de telles fonctions. Vous devez généralement tester plusieurs propriétés, mais vous pouvez souvent identifier un ensemble fini de propriétés qui, ensemble, valident le comportement correct d'une fonction sans nécessairement connaître le résultat attendu pour chaque cas.
L'une des meilleures introductions aux tests de propriétés que j'ai vues est celle-ci en F #. Espérons que la syntaxe ne soit pas un obstacle à la compréhension de l'explication de la technique.
Il est tentant d'écrire le code et de voir ensuite si le résultat "semble correct", mais, comme vous le pensez à juste titre, ce n'est pas une bonne idée.
Lorsque l'algorithme est difficile, vous pouvez faire un certain nombre de choses pour faciliter le calcul manuel du résultat.
Utilisez Excel. Configurez une feuille de calcul qui effectue tout ou partie du calcul pour vous. Restez assez simple pour pouvoir voir les étapes.
Divisez votre méthode en petites méthodes testables, chacune avec ses propres tests. Lorsque vous êtes sûr que les petites pièces fonctionnent, utilisez-les pour passer manuellement à l'étape suivante.
Utilisez des propriétés d'agrégat pour vérifier la validité. Par exemple, supposons que vous ayez un calculateur de probabilité; vous ne savez peut-être pas quels devraient être les résultats individuels, mais vous savez qu'ils doivent tous totaliser jusqu'à 100%.
Force brute. Écrivez un programme qui génère tous les résultats possibles et vérifiez qu'aucun n'est meilleur que ce que votre algorithme génère.
Dirigez-vous vers la section "essais comparatifs" pour obtenir des conseils qui ne figurent pas dans d'autres réponses.
Commencez par tester les cas qui devraient être rejetés par l'algorithme (zéro ou négatif workPerDay
, par exemple) et les cas qui sont triviaux (par exemple tableau tasks
vide).
Après cela, vous voulez d'abord tester les cas les plus simples. Pour l'entrée tasks
, nous devons tester différentes longueurs; il devrait suffire de tester les éléments 0, 1 et 2 (2 appartient à la catégorie "plusieurs" pour ce test).
Si vous pouvez trouver des données qui peuvent être calculées mentalement, c'est un bon début. Une technique que j'utilise parfois consiste à partir d'un résultat souhaité et à revenir (dans la spécification) aux entrées qui devraient produire ce résultat.
Parfois, la relation entre la sortie et l'entrée n'est pas évidente, mais vous avez une relation prévisible entre différentes sorties lorsqu'une entrée est modifiée. Si j'ai bien compris l'exemple, l'ajout d'une tâche (sans modifier les autres entrées) n'augmentera jamais la proportion de travail effectué à temps, nous pouvons donc créer un test qui appelle la fonction deux fois - une fois avec et une fois sans la tâche supplémentaire - et affirme l'inégalité entre les deux résultats.
Parfois, j'ai dû recourir à un long commentaire montrant un résultat calculé à la main dans les étapes correspondant à la spécification (un tel commentaire est généralement plus long que le cas de test). Le pire des cas est lorsque vous devez maintenir la compatibilité avec une implémentation antérieure dans un langage différent ou pour un environnement différent. Parfois, il suffit d'étiqueter les données de test avec quelque chose comme /* derived from v2.6 implementation on ARM system */
. Ce n'est pas très satisfaisant, mais peut être acceptable comme test de fidélité lors du portage ou comme béquille à court terme.
L'attribut le plus important d'un test est sa lisibilité - si les entrées et les sorties sont opaques pour le lecteur, alors le test a une valeur très faible, mais si le lecteur est aidé à comprendre les relations entre elles, alors le test remplit deux fonctions.
N'oubliez pas d'utiliser un "approximativement égal" approprié pour des résultats inexacts (par exemple virgule flottante).
Évitez les sur-tests - ajoutez un test uniquement s'il couvre quelque chose (comme une valeur limite) qui n'est pas atteint par d'autres tests.
Ce type de fonction difficile à tester n'a rien de très spécial. Il en va de même pour le code qui utilise des interfaces externes (par exemple, une API REST d'une application tierce qui n'est pas sous votre contrôle et certainement pas à tester par votre suite de tests; ou en utilisant un Bibliothèque tierce où vous n'êtes pas sûr du format d'octet exact des valeurs de retour).
C'est une approche tout à fait valable pour simplement exécuter votre algorithme pour une entrée saine, voir ce qu'il fait, vous assurer que le résultat est correct et encapsuler l'entrée et le résultat comme cas de test. Vous pouvez le faire pour quelques cas et ainsi obtenir plusieurs échantillons. Essayez de rendre les paramètres d'entrée aussi différents que possible. Dans le cas d'un appel d'API externe, vous effectueriez quelques appels par rapport au système réel, les traceriez avec un outil, puis les simulez dans vos tests unitaires pour voir comment votre programme réagit - ce qui revient au même que d'en sélectionner quelques-uns exécute votre code de planification des tâches, en les vérifiant à la main, puis en codant en dur le résultat dans vos tests.
Ensuite, évidemment, apportez des cas Edge comme (dans votre exemple) une liste vide de tâches; des choses comme ça.
Votre suite de tests ne sera peut-être pas aussi performante que pour une méthode où vous pouvez facilement prédire les résultats; mais toujours 100% mieux qu'aucune suite de tests (ou juste un test de fumée).
Si votre problème, cependant, est que vous avez du mal à décider si un résultat est correct, alors c'est un problème tout à fait différent. Par exemple, supposons que vous ayez une méthode qui détecte si un nombre arbitrairement grand est premier. Vous pouvez à peine y jeter un nombre aléatoire et simplement "regarder" si le résultat est correct (en supposant que vous ne pouvez pas décider de la primauté dans votre tête ou sur un morceau de papier). Dans ce cas, il y a en effet peu de choses que vous pouvez faire - vous devez soit obtenir des résultats connus (c.-à-d. De grands nombres premiers), soit implémenter la fonctionnalité avec un algorithme différent (peut-être même une équipe différente - la NASA semble aimer cela) et espérons que si l'une ou l'autre implémentation est boguée, au moins le bogue ne conduira pas aux mêmes mauvais résultats.
Si c'est un cas normal pour vous, alors vous devez avoir une bonne discussion avec vos ingénieurs des exigences. S'ils ne peuvent pas formuler vos besoins d'une manière facile (ou du moins possible) à vérifier pour vous, alors quand savez-vous si vous avez terminé?
Les autres réponses sont bonnes, je vais donc essayer de revenir sur certains points qu'ils ont collectivement manqués jusqu'à présent.
J'ai écrit (et testé de manière approfondie) un logiciel pour effectuer le traitement d'image à l'aide du radar à synthèse d'ouverture (SAR). C'est de nature scientifique/numérique (il y a beaucoup de géométrie, de physique et de mathématiques impliqués).
1) Utilisez des inverses. Quel est le fft
de [1,2,3,4,5]
? Aucune idée. Qu'est-ce que ifft(fft([1,2,3,4,5]))
? Doit être [1,2,3,4,5]
(Ou proche de lui, des erreurs en virgule flottante peuvent apparaître). Il en va de même pour le cas 2D.
2) Utilisez des assertions connues. Si vous écrivez une fonction déterminante, il pourrait être difficile de dire quel est le déterminant d'une matrice aléatoire 100x100. Mais vous savez que le déterminant de la matrice d'identité est 1, même si c'est 100x100. Vous savez également que la fonction doit retourner 0 sur une matrice non inversible (comme un 100x100 plein de tous les 0).
) Utilisez des assertions approximatives au lieu d'assertions exactes . J'ai écrit un code pour ledit traitement SAR qui enregistrerait deux images en générant des points de liaison qui créent une cartographie entre les images, puis effectuent une déformation entre elles pour les faire correspondre. Il pourrait s'inscrire à un niveau sous-pixel. A priori, il est difficile de dire quoi que ce soit à quoi pourrait ressembler l'enregistrement de deux images. Comment pouvez-vous le tester? Des choses comme:
EXPECT_TRUE(register(img1, img2).size() < min(img1.size(), img2.size()))
puisque vous ne pouvez vous inscrire que sur des parties qui se chevauchent, l'image enregistrée doit être plus petite ou égale à votre plus petite image, et aussi:
scale = 255
EXPECT_PIXEL_EQ_WITH_TOLERANCE(reg(img, img), img, .05*scale)
car une image enregistrée sur elle-même doit être proche d'elle-même, mais vous pourriez rencontrer un peu plus que des erreurs en virgule flottante en raison de l'algorithme à portée de main, alors vérifiez simplement que chaque pixel se trouve à +/- 5% de la plage que les pixels peuvent prendre (0-255 est une échelle de gris, courante dans le traitement d'image). Le résultat doit au moins être de la même taille que l'entrée.
Vous pouvez même simplement tester la fumée (c'est-à-dire l'appeler et vous assurer qu'elle ne plante pas). En général, cette technique est meilleure pour les tests plus volumineux où le résultat final ne peut pas être (facilement) calculé a priori avant d'exécuter le test.
4) Utilisez OR STORE une graine de nombre aléatoire pour votre RNG.
Les exécutions doivent être reproductibles. Il est faux, cependant, que la seule façon d'obtenir une exécution reproductible est de fournir une graine spécifique à un générateur de nombres aléatoires. Parfois, les tests de hasard sont précieux. J'ai vu/entendu parler de bogues dans le code scientifique qui surviennent dans des cas dégénérés qui ont été générés au hasard (dans des algorithmes complexes, il peut être difficile de voir ce qu'est le cas dégénéré ) ). Au lieu d'appeler toujours votre fonction avec la même graine, générez une graine aléatoire, puis utilisez cette graine et enregistrez la valeur de la graine. De cette façon, chaque exécution a une graine aléatoire différente, mais si vous obtenez un plantage, vous pouvez réexécuter le résultat en utilisant la graine que vous avez enregistrée pour déboguer. J'ai effectivement utilisé cela dans la pratique et cela a corrigé un bug, alors j'ai pensé que je le mentionnerais. Certes, cela n'est arrivé qu'une seule fois, et je suis certain que cela ne vaut pas toujours la peine, alors utilisez cette technique avec prudence. Aléatoire avec la même graine est toujours sûr, cependant. Inconvénient (au lieu d'utiliser simplement la même graine tout le temps): Vous devez enregistrer vos cycles de test. Côté positif: correction et correction des bogues.
1) Test qu'un videtaskArray
retourne 0 (assertion connue).
2) Générer une entrée aléatoire telle quetask.time > 0
, task.due > 0
, ettask.importance > 0
pour tous = task
s, et affirmer que le résultat est supérieur à0
(assertion approximative, entrée aléatoire). Vous n'avez pas besoin de devenir fou et de générer des graines aléatoires, votre algorithme n'est tout simplement pas assez complexe pour le justifier. Il y a environ 0 chance que cela rapporte: gardez simplement le test simple.
3) Testez sitask.importance == 0
pour toustask
s, alors le résultat est0
- (affirmation connue)
4) D'autres réponses ont touché à ce sujet, mais cela pourrait être important pour votre cas particulier : Si vous créez une API à utiliser par des utilisateurs extérieurs à votre équipe, vous devez tester les cas dégénérés. Par exemple, si workPerDay == 0
, assurez-vous de lancer une belle erreur qui indique à l'utilisateur que les entrées ne sont pas valides. Si vous ne créez pas d'API et que c'est uniquement pour vous et votre équipe, vous pouvez probablement ignorer cette étape et refuser de l'appeler avec le cas dégénéré.
HTH.
Intégrez des tests d'assertion dans votre suite de tests unitaires pour des tests basés sur les propriétés de votre algorithme. En plus d'écrire des tests unitaires qui vérifient une sortie spécifique, écrivez des tests conçus pour échouer en déclenchant des échecs d'assertion dans le code principal.
De nombreux algorithmes s'appuient pour leurs preuves d'exactitude sur le maintien de certaines propriétés tout au long des étapes de l'algorithme. Si vous pouvez vérifier sensiblement ces propriétés en regardant la sortie d'une fonction, le test unitaire suffit à lui seul pour tester vos propriétés. Sinon, les tests basés sur les assertions vous permettent de tester qu'une implémentation conserve une propriété à chaque fois que l'algorithme l'assume.
Les tests basés sur des assertions exposeront les défauts d'algorithme, les bogues de codage et les échecs d'implémentation en raison de problèmes tels que l'instabilité numérique. De nombreux langages ont des mécanismes qui suppriment les assertions au moment de la compilation ou avant que le code ne soit interprété de sorte que lorsqu'elles sont exécutées en mode production, les assertions n'entraînent pas de pénalité de performances. Si votre code réussit les tests unitaires mais échoue dans un cas réel, vous pouvez réactiver les assertions en tant qu'outil de débogage.
Certaines des autres réponses ici sont très bonnes:
... J'ajouterais quelques autres tactiques:
La décomposition vous permet de vous assurer que les composants de votre algorithme font ce que vous attendez d'eux. Et une "bonne" décomposition vous permet également de vous assurer qu'ils sont collés correctement. Une grande décomposition généralise et simplifie l'algorithme dans la mesure où vous pouvez prévoir les résultats (des algorithmes génériques simplifiés) à la main suffisamment bien pour écrire des tests approfondis.
Si vous ne pouvez pas vous décomposer dans cette mesure, prouvez l'algorithme en dehors du code par tout moyen suffisant pour vous satisfaire, vous et vos pairs, les parties prenantes et les clients. Et puis, décomposez juste assez pour prouver que votre implémentation correspond à la conception.
Cela peut sembler être une réponse idéaliste, mais cela aide à identifier différents types de tests.
Si des réponses strictes sont importantes pour l'implémentation, des exemples et des réponses attendues doivent vraiment être fournis dans les exigences qui décrivent l'algorithme. Ces exigences doivent être examinées en groupe et si vous n'obtenez pas les mêmes résultats, la raison doit être identifiée.
Même si vous jouez le rôle d'analyste et d'implémentateur, vous devez en fait créer des exigences et les faire réviser bien avant d'écrire des tests unitaires, dans ce cas, vous connaîtrez les résultats attendus et pourrez écrire vos tests en conséquence.
D'un autre côté, s'il s'agit d'une partie que vous implémentez qui ne fait pas partie de la logique métier ou prend en charge une réponse de logique métier, il devrait être correct d'exécuter le test pour voir quels sont les résultats, puis modifier le test pour vous attendre ces résultats. Les résultats finaux sont déjà vérifiés par rapport à vos besoins, donc s'ils sont corrects, alors tout le code alimentant ces résultats finaux doit être numériquement correct et à ce stade, vos tests unitaires visent davantage à détecter les cas de défaillance d'Edge et les futurs changements de refactorisation qu'à prouver qu'une donnée l'algorithme produit des résultats corrects.
Je pense qu'il est parfaitement acceptable à certaines occasions de suivre le processus:
Il s'agit d'une approche raisonnable dans toute situation où la vérification de l'exactitude d'une réponse à la main est plus facile que de calculer la réponse à la main à partir des premiers principes.
Je connais des gens qui écrivent des logiciels pour rendre des pages imprimées et qui ont des tests qui vérifient que les bons pixels sont définis sur la page imprimée. La seule façon sensée de le faire est d'écrire le code pour rendre la page, de vérifier à l'œil qu'il semble bon, puis de capturer le résultat comme un test de régression pour les versions futures.
Ce n'est pas parce que vous lisez dans un livre qu'une méthodologie particulière encourage d'abord l'écriture des cas de test que vous devez toujours le faire de cette façon. Les règles sont là pour être enfreintes.
Autres réponses Les réponses ont déjà des techniques pour ce à quoi ressemble un test lorsque le résultat spécifique ne peut pas être déterminé en dehors de la fonction testée.
Ce que je fais en plus que je n'ai pas repéré dans les autres réponses, c'est de générer automatiquement des tests d'une manière ou d'une autre:
Par exemple, si la fonction prend trois paramètres chacun avec la plage d'entrée autorisée [-1,1], testez toutes les combinaisons de chaque paramètre, {-2, -1.01, -1, -0.99, -0.5, -0.01, 0,0.01 , 0,5,0,99,1,1,01,2, certains plus aléatoires en (-1,1)}
En bref: Parfois la mauvaise qualité peut être subventionnée par la quantité.