Je travaille fréquemment avec des programmes très numériques / mathématiques, où le résultat exact d'une fonction est difficile à prédire à 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 le seul moyen de connaître le résultat attendu est d'appliquer l'algorithme lui-même (que ce soit dans mon cas). tête, sur papier ou par ordinateur). Cela me semble erroné, car j’utilise efficacement le code testé pour vérifier mes tests unitaires, et non 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:
Fonction weightedTasksOnTime
qui, en fonction de la quantité de travail effectué par jour workPerDay
dans la plage (0, 24], de l'heure actuelle initialTime
> 0 et d'une liste de tâches taskArray
, chacune avec un délai 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 l'intervalle [0, 1] représentant l'importance des tâches pouvant être terminées avant leur due
date si chaque tâche est complétée dans l'ordre indiqué en taskArray
commençant à initialTime
.
L'algorithme pour implémenter cette fonction est relativement simple: itérer sur des 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 renvoyer l'accumulateur, divisez par la somme des importances de tâches à 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 en normalisant l'exigence, 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 concerne les situations dans lesquelles le code testé n'est pas une nouvelle implémentation d'un algorithme existant. Si le code est une réimplémentation, il est intrinsèquement facile de prédire les résultats, car les implémentations fiables de l'algorithme agissent comme un oracle de test naturel.
la source
Réponses:
Il y a deux choses que vous pouvez tester dans un code difficile à tester. Premièrement, 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 qu'un seul soit passé la date d'échéance, etc. Tout ce qui est plus simple que votre vrai problème, mais qui reste raisonnable à calculer manuellement.
La seconde est la vérification de la santé mentale. Ce sont les vérifications que vous faites lorsque vous ne savez pas si une réponse est bonne , mais vous sauriez certainement si c'est faux . 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 gâchez les contrôles de cohérence et les cas dégénérés, ce qui révèle un problème dans votre algorithme complet.
la source
J'avais l'habitude d'écrire des tests pour des logiciels scientifiques avec des résultats difficiles à prédire. Nous avons beaucoup utilisé les relations métamorphiques. En gros, vous savez certaines choses sur la manière dont votre logiciel devrait se comporter même si vous ne connaissez pas les sorties numériques exactes.
Un exemple possible pour votre cas: si vous réduisez la quantité de travail que vous pouvez faire chaque jour, la quantité totale de travail que vous pouvez faire restera au mieux inchangée, mais diminuera probablement. Exécutez donc la fonction pour un nombre de valeurs de
workPerDay
et assurez-vous que la relation est valide.la source
Les autres réponses ont de bonnes idées pour développer des tests de bord ou d’erreur. Pour les autres, utiliser l'algorithme lui-même n'est pas idéal (évidemment) mais reste 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 a été délibéré, vous devez revoir le test unitaire.
la source
De la même manière que vous écrivez des tests unitaires pour tout autre type de code:
À moins que votre code ne comporte un élément aléatoire ou ne soit pas déterministe (par exemple, 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 affectées par des forces extérieures. Les fonctions pures sont plus faciles à tester.
la source
Unless your code involves some random element
Le truc ici consiste à faire de votre générateur de nombres aléatoires une dépendance injectée, de sorte que vous puissiez ensuite le remplacer par un générateur de nombres donnant le résultat exact que vous souhaitez. Cela vous permet de tester à nouveau avec précision - en comptant également les nombres générés comme paramètres d'entrée.not deterministic (i.e. it won't produce the same output given the same input)
Dans la mesure où un test unitaire doit partir d'une situation contrôlée , il ne peut être non déterministe que s'il comporte un élément aléatoire, que vous pouvez ensuite injecter. Je ne peux pas penser à d'autres possibilités ici.if(x == x)
, c'est une comparaison inutile. Vous avez besoin que vos deux résultats ( réels : proviennent du code; attendus : proviennent de vos connaissances externes) pour être indépendants l'un de l'autre.Mise à jour en raison de commentaires postés
La réponse originale a été supprimée par souci de brièveté - vous pouvez la trouver dans l'historique des modifications.
Tout d’abord, un TL; DR pour éviter une réponse longue par ailleurs:
Le problème principal 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 de l'entreprise.
Par exemple, le client veut que cela fonctionne comme ceci . Cependant, le développeur comprend mal, et il écrit un code qui fait ça .
Le développeur va donc écrire des tests unitaires qui testent si [cela] fonctionne comme prévu. S'il a développé l'application correctement, ses tests unitaires passeront même si l'application ne fait pas [ce] , que le client attendait.
Si vous souhaitez tester les attentes du client (les exigences de l'entreprise), vous devez le faire dans une étape distincte (et ultérieure).
Un workflow de développement simple pour vous indiquer quand ces tests doivent être exécutés:
Vous vous demandez peut-être ce qu’il ya à faire deux tests distincts lorsque le client et le développeur sont identiques. Puisqu'il n'y a pas de "transfert" de développeur à client, les tests sont exécutés les uns après les autres, mais il s'agit toujours d'étapes distinctes.
Si vous voulez vérifier si votre algorithme est correct, cela ne fait pas partie du travail du développeur . C’est la préoccupation du client, qui le testera à l’ aide de l’application.
En tant qu’entrepreneur et universitaire, vous risquez de manquer une distinction importante qui met en évidence les différentes responsabilités.
la source
Test de propriété
Parfois, les fonctions mathématiques sont mieux servies par "Property Testing" que par les tests unitaires classiques basés sur des exemples. Par exemple, imaginez que vous écrivez des tests unitaires pour quelque chose comme une fonction "multiplier" entière. Alors que la fonction elle-même peut sembler très simple, si c'est le seul moyen de se multiplier, comment la testez-vous complètement sans la logique de la fonction elle-même? Vous pouvez utiliser des tables géantes avec les entrées / sorties attendues, mais ceci 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 multiplier un nombre négatif et un nombre positif devrait donner un nombre négatif, et que multiplier deux nombres négatifs donnerait un nombre positif, etc. Utiliser des valeurs aléatoires, puis vérifier que ces propriétés sont préservées pour tous 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 vue est celle-ci en fa #. Espérons que la syntaxe ne soit pas un obstacle à la compréhension de l'explication de la technique.
la source
Il est tentant d'écrire le code et de voir ensuite si le résultat "semble bon", mais, comme vous l'avez intuitivement compris, ce n'est pas une bonne idée.
Lorsque l'algorithme est difficile, vous pouvez effectuer 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 que vous puissiez voir les étapes.
Divisez votre méthode en petites méthodes testables, chacune avec leurs 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 globales pour vérifier votre santé. Par exemple, supposons que vous ayez un calculateur de probabilité; vous ne savez peut-être pas quels résultats individuels devraient être, mais vous savez qu'ils doivent tous faire un total de 100%.
Force brute. Ecrivez un programme qui génère tous les résultats possibles et vérifiez qu'aucun d'entre eux n'est supérieur à ce que votre algorithme génère.
la source
TL; DR
Rendez-vous à la section "Tests comparatifs" pour obtenir des conseils qui ne figurent pas dans les autres réponses.
Débuts
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, untasks
tableau vide ).Après cela, vous souhaitez d’abord tester les cas les plus simples. Pour l'
tasks
entrée, nous devons tester différentes longueurs; il devrait suffire de tester les éléments 0, 1 et 2 (2 appartiennent à la catégorie "nombreux" pour cet essai).Si vous pouvez trouver des entrées qui peuvent être calculées mentalement, c'est un bon début. Une technique que j'utilise parfois est de partir d'un résultat souhaité et de revenir (dans la spécification) aux entrées qui devraient produire ce résultat.
Tests comparatifs
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 d'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 sans tâche supplémentaire. - et affirme l'inégalité entre les deux résultats.
Les replis
Parfois, je devais avoir recours à un long commentaire indiquant un résultat calculé manuellement dans les étapes correspondant à la spécification (un tel commentaire est généralement plus long que le scénario 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, vous devez simplement é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.Des rappels
L'attribut le plus important d'un test est sa lisibilité - si les entrées et les sorties sont opaques pour le lecteur, le test a une valeur très faible, mais si on aide le lecteur à comprendre les relations entre elles, le test remplit deux objectifs.
N'oubliez pas d'utiliser un "approximativement égal" pour obtenir des résultats inexacts (par exemple, une virgule flottante).
Évitez les tests excessifs - ajoutez un test uniquement s'il couvre quelque chose (tel qu'une valeur limite) que d'autres tests n'atteignent pas.
la source
Ce type de fonction difficile à tester n'a rien de très particulier. 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 qui n’a certainement pas été testée par votre suite de tests; ou l’utilisation d’une bibliothèque tierce où vous n'êtes pas sûr de la format d'octet exact des valeurs de retour).
Il est tout à fait judicieux d’exécuter simplement votre algorithme pour une entrée saine, de voir ce qu’il fait, de s’assurer que le résultat est correct et d’encapsuler l’entrée et le résultat sous forme de scénario de test. Vous pouvez le faire pour quelques cas et obtenir ainsi 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 pouvez effectuer quelques appels sur le système réel, les suivre à l'aide d'un outil, puis les imiter dans vos tests unitaires pour voir comment votre programme réagit - ce qui revient à choisir 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, bien sûr, introduisez des cas extrêmes tels que (dans votre exemple) une liste vide de tâches; des choses comme ça.
Votre suite de tests ne sera peut-être pas aussi performante qu'une méthode permettant de prédire facilement les résultats. mais toujours 100% mieux que pas de 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, le problème est totalement différent. Par exemple, supposons que votre méthode détecte si un nombre arbitrairement grand est premier. Vous pouvez difficilement jeter un nombre aléatoire dessus et ensuite juste "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, vous ne pouvez en effet que peu de choses à faire: vous devez obtenir des résultats connus (c'est-à-dire des nombres premiers importants), ou implémenter la fonctionnalité avec un algorithme différent (peut-être même une équipe différente - la NASA semble aimer beaucoup ça) et espère que si l’une des implémentations est boguée, au moins le bogue ne conduit pas aux mêmes résultats erronés.
S'il s'agit d'un cas habituel pour vous, vous devez avoir une conversation sérieuse avec vos ingénieurs des exigences. S'ils ne peuvent pas formuler vos exigences de manière simple (ou possible) à vérifier pour vous, alors quand savez-vous si vous avez terminé?
la source
Les autres réponses sont bonnes, alors je vais essayer de revenir sur certains points qu'ils ont manqués collectivement jusqu'à présent.
J'ai écrit (et testé minutieusement) un logiciel permettant de traiter des images à l'aide d'un radar à synthèse d'ouverture (SAR). C'est de nature scientifique / numérique (beaucoup de géométrie, de physique et de mathématiques sont impliquées).
Quelques conseils (pour les tests scientifiques / numériques généraux):
1) Utilisez des inverses. Quel est le
fft
de[1,2,3,4,5]
? Aucune idée. C'est quoiifft(fft([1,2,3,4,5]))
? Devrait être[1,2,3,4,5]
(ou près de lui, des erreurs de virgule flottante peuvent survenir). Il en va de même pour le cas 2D.2) Utilisez des assertions connues. Si vous écrivez une fonction déterminante, il sera peut-ê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 renvoyer 0 sur une matrice non inversible (comme un 100x100 plein de tous les 0).
3) Utilisez des assertions grossières au lieu d' affirmations exactes . J'ai écrit un code pour ledit traitement SAR qui enregistrerait deux images en générant des points de rattachement qui créent un mappage entre les images, puis en effectuant une distorsion entre elles pour les faire correspondre. Il pourrait s’inscrire au niveau des sous-pixels. A priori, il est difficile de dire quoi que ce soit à quoi ressemblerait l'enregistrement de deux images. Comment pouvez-vous le tester? Des choses comme:
étant donné que 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:
puisqu’une image enregistrée sur elle-même doit être proche de elle-même, mais que vous risquez de rencontrer un peu plus que des erreurs en virgule flottante dues à l’algorithme utilisé. (0-255 est en niveaux de gris, commun 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 fumer test (c'est-à-dire l'appeler et assurez-vous qu'il ne tombe pas en panne). En général, cette technique est préférable pour les tests de grande taille où le résultat final ne peut pas être (facilement) calculé avant la réalisation du test.
4) Utilisez OU STORE une graine de nombre aléatoire pour votre RNG.
Runs ne doivent être reproductibles. Il est faux, cependant, que le seul moyen d'obtenir une exécution reproductible consiste à fournir une graine spécifique à un générateur de nombres aléatoires. Parfois, les tests aléatoires sont précieux. Je l' ai vu / entendu parler des bugs dans le code scientifique qui surgissent dans les cas dégénérés qui ont été générés au hasard (dans les algorithmes complexes , il peut être difficile de voir ce que le cas dégénéré même est). 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 sa valeur. De cette façon, chaque exécution a une graine aléatoire différente, mais si vous rencontrez un blocage, vous pouvez réexécuter le résultat en utilisant la graine que vous avez enregistrée pour déboguer. En fait, j'ai utilisé cela dans la pratique et cela a éliminé un bogue, alors je me suis dit que je le mentionnerais. Certes, cela n'est arrivé qu'une fois, et je suis convaincu 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 consigner vos tests. Upside: Correction et correction de bugs.
Votre cas particulier
1) Vérifiez qu’un vide
taskArray
renvoie 0 (assert connu).2) Génération aléatoire d' entrée de telle sorte que
task.time > 0
,task.due > 0
, ettask.importance > 0
pour toustask
s, et affirmer le résultat est supérieur à0
(assertion rugueuse, 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 porte ses fruits: gardez simplement le test simple.3) Teste si
task.importance == 0
pour touttask
s, le résultat est0
(connu)4) D'autres réponses ont abordé ce sujet, mais cela pourrait être important pour votre cas particulier : Si vous créez une API destinée à être utilisée 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 créer une belle erreur qui indique à l'utilisateur que la saisie est invalide. Si vous ne créez pas d'API et que ce n'est que pour vous et votre équipe, vous pouvez probablement ignorer cette étape et simplement refuser de l'appeler avec la casse dégénérée.HTH.
la source
Intégrez les tests d'assertion à votre suite de tests unitaires pour tester vos algorithmes en fonction des propriétés. 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 reposent sur le maintien de certaines propriétés tout au long des étapes de l’algorithme. Si vous pouvez vérifier judicieusement ces propriétés en consultant le résultat d'une fonction, le test unitaire suffit à lui seul. Sinon, le test basé sur les assertions vous permet de vérifier qu'une implémentation conserve une propriété chaque fois que l'algorithme l'assume.
Les tests basés sur les assertions exposeront les failles des algorithmes, les bogues de codage et les échecs d'implémentation dus à des 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 l'interprétation du code, de sorte que, lorsqu'elles sont exécutées en mode de production, les assertions n'entraînent aucune perte 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.
la source
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 bien collés ensemble. Une bonne décomposition généralise et simplifie l'algorithme dans la mesure où vous pouvez prédire les résultats (du ou des algorithmes génériques simplifiés) suffisamment à la main pour écrire des tests approfondis.
Si vous ne pouvez pas vous décomposer à ce point, prouvez que l'algorithme en dehors du code, par tout moyen, est suffisant pour vous satisfaire, ainsi que vos pairs, les parties prenantes et les clients. Ensuite, décomposez suffisamment pour prouver que votre implémentation correspond à la conception.
la source
Cela peut sembler une réponse idéaliste, mais cela aide à identifier différents types de tests.
Si des réponses strictes sont importantes pour la mise en œuvre, les exigences décrivant l'algorithme doivent être accompagnées d'exemples et de réponses attendues. Ces exigences doivent être revues en groupe et si vous n'obtenez pas les mêmes résultats, vous devez identifier la raison.
Même si vous jouez le rôle d'analyste et de responsable de la mise en œuvre, vous devez réellement 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é, si vous implémentez cette pièce qui ne fait pas partie de la logique métier ou prend en charge une réponse de la logique métier, il devrait être correct d'exécuter le test pour voir quels sont les résultats, puis de modifier le test en conséquence. ces résultats. Les résultats finaux sont déjà vérifiés par rapport à vos exigences. Par conséquent, s’ils sont corrects, tous les codes qui les alimentent doivent être numériquement corrects. À ce stade, vos tests unitaires sont davantage destinés à détecter les défaillances de bord et les modifications de refactoring futures que de prouver qu’un paramètre donné est utilisé. L'algorithme produit des résultats corrects.
la source
Je pense qu'il est parfaitement acceptable en certaines occasions de suivre le processus:
C'est une approche raisonnable dans toutes les situations où il est plus facile de vérifier l'exactitude d'une réponse à la main que de la calculer manuellement à partir des principes premiers.
Je connais des gens qui écrivent des logiciels pour le rendu 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 restituer la page, de vérifier à l'œil nu qu'il semble bon, puis de capturer le résultat en tant que 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 la rédaction des cas de test que vous devez toujours le faire de cette façon. Les règles sont là pour être enfreintes.
la source
D'autres réponses contiennent déjà des techniques permettant de définir à 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, ce que je n'ai pas remarqué dans les autres réponses, est de générer automatiquement des tests d'une certaine manière:
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.5, 0,0.01. , 0.5,0.99,1,1.01,2, un peu plus aléatoire en (-1,1)}
En bref: la qualité médiocre peut parfois être subventionnée par quantité.
la source