Comment écrivez-vous des tests unitaires pour le code avec des résultats difficiles à prévoir?

124

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 weightedTasksOnTimequi, en fonction de la quantité de travail effectué par jour workPerDaydans 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 dueet 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 duedate si chaque tâche est complétée dans l'ordre indiqué en taskArraycommenç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 workPerDayet 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.

PaintingInAir
la source
4
Pouvez-vous donner un exemple simple d'une fonction dont le résultat est difficile à prédire?
Robert Harvey
62
FWIW vous ne testez pas l'algorithme. Je suppose que c'est correct. Vous testez la mise en œuvre. Travailler à la main est souvent bien comme une construction parallèle.
Kristian H
7
Il existe des situations dans lesquelles un algorithme ne peut pas être testé raisonnablement par unité - par exemple si son temps d'exécution est de plusieurs jours / mois. Cela peut arriver lors de la résolution de problèmes NP. Dans ces cas, il peut être plus pratique de fournir une preuve formelle que le code est correct.
Hulk
12
Quelque chose que j'ai vu dans un code numérique très délicat consiste à traiter les tests unitaires uniquement comme des tests de régression. Ecrivez la fonction, lancez-la pour plusieurs valeurs intéressantes, validez les résultats manuellement, puis écrivez le test unitaire pour intercepter les régressions à partir du résultat attendu. Coder l'horreur? Curieux de savoir ce que les autres pensent.
Chuu

Réponses:

251

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.

Karl Bielefeldt
la source
54
Pense que c'est un très bon conseil. Commencez par écrire ces types de tests unitaires. Lorsque vous développez le logiciel, si vous trouvez des bogues ou des réponses incorrectes, ajoutez-les sous forme de tests unitaires. Faites la même chose, dans une certaine mesure, lorsque vous trouvez les bonnes réponses. Construisez-les au fil du temps, et vous aurez (éventuellement) une série très complète de tests unitaires bien que vous ne commenciez pas à savoir ce qu'ils allaient être ...
Algy Taylor
21
Une autre chose qui peut être utile dans certains cas (bien que peut-être pas celui-ci) est d'écrire une fonction inverse et de vérifier que, lorsqu'elles sont chaînées, vos entrées et vos sorties sont identiques.
Cyberspark
7
Les vérifications de sécurité constituent souvent de bonnes cibles pour les tests basés sur les propriétés avec quelque chose comme QuickCheck
jk.
10
Parmi les autres tests que je recommanderais, il y en a quelques-uns qui permettent de vérifier les modifications non intentionnelles de la sortie. Vous pouvez les "tricher" en utilisant le code lui-même pour générer le résultat attendu, car il est destiné à aider les mainteneurs en signalant que quelque chose de censé être une modification neutre de la sortie a affecté de manière non intentionnelle le comportement algorithmique.
Dan Neely
5
@ iFlo Vous n'êtes pas sûr de plaisanter, mais l'inverse inverse existe déjà. Il faut savoir que l'échec du test pourrait poser problème dans la fonction inverse
lucidbrot
80

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 workPerDayet assurez-vous que la relation est valide.

James Elderfield
la source
32
Les relations métamorphiques sont un exemple spécifique de test basé sur les propriétés , qui est en général un outil utile dans de telles situations
Dannnno
38

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.

utilisateur949300
la source
6
Et pour mémoire, ces types de tests sont souvent appelés "tests de régression" en fonction de leur objectif, et constituent fondamentalement un filet de sécurité pour toute modification / refactorisation.
Pac0
21

De la même manière que vous écrivez des tests unitaires pour tout autre type de code:

  1. Trouvez des cas de test représentatifs et testez-les.
  2. Trouvez des cas extrêmes et testez-les.
  3. Recherchez les conditions d'erreur et testez-les.

À 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.

Robert Harvey
la source
2
Pour les algorithmes non déterministes, vous pouvez sauvegarder la graine de RNG ou la simuler à l'aide de séries déterministes à séquence fixe ou à faible discordance, par exemple la séquence de Halton
Merveilleux
14
@PaintingInAir S'il est impossible de vérifier la sortie de l'algorithme, l'algorithme peut-il même être incorrect?
WolfgangGroiss
5
Unless your code involves some random elementLe 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.
Flater
3
@PaintingInAir: ou. Mon commentaire s’applique aussi bien à l’exécution rapide qu’à l’écriture rapide de tests. Si cela vous prend trois jours pour calculer un seul exemple à la main (supposons que vous utilisiez la méthode la plus rapide disponible sans utiliser le code), trois jours suffiront. Si, au lieu de cela, vous basez le résultat attendu du test sur le code lui-même, le test se compromet. C'est comme faire 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.
Flater
2
Il est toujours testable sur l'unité même s'il n'est pas déterministe, à condition qu'il soit conforme aux spécifications et que cette conformité puisse être mesurée (par exemple, distribution et propagation aléatoires). Il peut suffire d'exiger un grand nombre d'échantillons pour éliminer le risque d'anomalie.
mckenzm
17

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.

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ées destiné à optimiser la qualité d'un ordre de tâches. En termes de description de la nécessité de la fonction d’exemple en interne: «j’ai besoin d’une fonction objective pour maximiser l’importance des tâches terminées à temps». Cependant, il semble toujours y avoir un grand fossé entre cette demande et la mise en œuvre des tests unitaires.

Tout d’abord, un TL; DR pour éviter une réponse longue par ailleurs:

Pensez-y de la manière suivante:
un client entre dans McDonald 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 hamburger exactement comme demandé. Le client reçoit ce burger, le mange, puis se plaint au cuisinier qu'il ne s'agit pas d'un burger savoureux!

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 réellement bonne . Le cuisinier crée simplement ce que le client commande. Il incombe au client de commander quelque chose qu’il trouve savoureux .

De même, le développeur n'a pas à s'interroger sur l'exactitude de l'algorithme. Leur seul travail consiste à implémenter l'algorithme tel que demandé.
Les tests unitaires sont un outil de développement. Cela confirme que le burger correspond à la commande (avant de quitter la cuisine). Il ne tente pas (et ne devrait pas) essayer de confirmer que le hamburger commandé est réellement délicieux.

Même si vous êtes à la fois le client et le cuisinier, il y a toujours une distinction significative entre:

  • Je n'ai pas préparé ce repas correctement, il n'était pas délicieux (= erreur de cuisson). Un steak brûlé ne sera jamais bon au goût, même si vous aimez le steak.
  • J'ai préparé le repas correctement, mais je ne l'aime pas (= erreur du client). Si vous n'aimez pas le steak, vous n'aimerez jamais en manger, même si vous l'avez cuit à la perfection.

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:

  • Le client explique le problème qu'il veut résoudre.
  • L'analyste (ou le développeur) écrit cela dans une analyse.
  • Le développeur écrit un code qui fait ce que l'analyse décrit.
  • Le développeur teste son code (tests unitaires) pour voir s'il a correctement suivi l'analyse.
  • Si les tests unitaires échouent, le développeur reprend son développement. Cela boucle indéfiniment, jusqu'à ce que l'unité teste tous les tests.
  • Ayant maintenant une base de code testée (confirmée et passée), le développeur construit l'application.
  • L'application est donnée au client.
  • Le client vérifie maintenant si l’application qui lui est proposée résout réellement le problème qu’il cherchait à résoudre (tests d’assurance qualité) .

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.

  • Les tests unitaires sont un outil spécialisé qui vous aide à vérifier si votre phase de développement est terminée.
  • Les tests d'assurance qualité sont effectués à l'aide de l'application .

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.

  • Si l'application ne respecte pas ce que le client avait initialement demandé, les modifications ultérieures du code sont généralement effectuées sans frais . car c'est une erreur de développement. Le développeur a commis une erreur et doit payer le coût de la rectification.
  • Si l'application fait ce que le client avait initialement demandé, mais que le client a maintenant changé d'avis (par exemple, vous avez décidé d'utiliser un algorithme différent et meilleur), les modifications apportées à la base de codes sont facturées au client , car ce n'est pas le cas. faute du développeur que le client ait demandé quelque chose de différent de ce qu'il veut maintenant. Il incombe au client (coût) de changer d’avis et donc de faire en sorte que les développeurs consacrent plus d’efforts à développer quelque chose qui n’avait pas été convenu auparavant.
Flater
la source
Je serais heureux de voir plus de détails sur la situation "Si vous avez inventé vous-même l'algorithme", car je pense que c'est la situation la plus susceptible de poser des problèmes. Surtout dans les situations où aucun exemple "si A alors B, sinon C" n'est fourni. (ps je ne suis pas le votant)
PaintingInAir
@PaintingInAir: Mais je ne peux pas vraiment en dire plus, cela dépend de votre situation. Si vous avez décidé de créer cet algorithme, vous l’avez évidemment fait pour fournir une fonctionnalité particulière. Qui vous a demandé de le faire? Comment ont-ils décrit leur demande? Vous ont-ils dit ce dont ils avaient besoin dans certains scénarios? (Cette information est ce que j'appelle "l'analyse" dans ma réponse.) Quelle que soit l'explication que vous avez reçue (qui vous a amené à créer l'algorithme), vous pouvez vérifier si celui-ci fonctionne comme vous le souhaitez. En bref, tout, sauf le code / algorithme créé par soi-même, peut être utilisé.
Flater
2
@PaintingInAir: Il est dangereux d'associer étroitement le client, l'analyste et le développeur. vous êtes enclin à sauter des étapes essentielles telles que la définition du début du problème . Je crois que c'est ce que vous faites ici. Vous semblez vouloir vérifier l' exactitude de l'algorithme plutôt que de savoir s'il a été implémenté correctement. Mais ce n'est pas comme ça que vous le faites. Le test de la mise en œuvre peut être effectué à l'aide de tests unitaires. Tester l'algorithme lui-même consiste à utiliser votre application (testée) et à vérifier ses résultats - ce test réel sort du cadre de votre base de code (comme il se doit ).
Flater
4
Cette réponse est déjà énorme. Nous vous recommandons vivement d’essayer de trouver un moyen de reformuler le contenu original afin de l’intégrer dans la nouvelle réponse si vous ne voulez pas le jeter à la poubelle.
jpmc26
7
Aussi, je suis en désaccord avec votre prémisse. Les tests peuvent et doivent absolument révéler quand le code génère une sortie incorrecte conformément à la spécification. Il est valable pour les tests de valider les sorties pour certains cas de test connus. En outre, le cuisinier devrait savoir qu'il vaut mieux que de ne pas accepter le "savon pour les mains" en tant qu'ingrédient valide pour le hamburger; l'employeur a presque certainement informé le cuisinier des ingrédients disponibles.
jpmc26
9

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.

Aaron M. Eshbach
la source
1
Je suggérerais peut-être d'ajouter quelque chose d'un peu plus spécifique dans votre exemple de multiplication, comme générer des quatuors aléatoires (a, b, c) et confirmer que (ab) (cd) donne (ac-ad) - (bc-bd). Une opération de multiplication peut être assez perturbée tout en maintenant la règle (fois négatifs négatifs, positif), mais la règle distributive prédit des résultats spécifiques.
Supercat
4

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.

  1. 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.

  2. 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.

  3. 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%.

  4. 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.

Ewan
la source
Pour 3., permettez quelques erreurs d'arrondis ici. Il est possible que votre total se monte à 100,000001% ou à des chiffres similaires proches mais non exacts.
Flater
2
4. Si vous êtes en mesure de générer le résultat optimal pour toutes les combinaisons d'entrées possibles (que vous utiliserez ensuite pour la confirmation du test), vous êtes par nature déjà capable de calculer le résultat optimal et, par conséquent, de ne pas le faire. Vous n’avez pas besoin de ce second code de code que vous essayez de tester. À ce stade, vous feriez mieux d'utiliser votre générateur de résultats optimal existant, car il a déjà fait ses preuves. (et si cela n'a pas encore fait ses preuves, vous ne pouvez pas compter sur ses résultats pour vérifier vos tests pour commencer).
Flater
6
@flater généralement vous avez d'autres exigences ainsi que l'exactitude que la force brutale ne répond pas. par exemple la performance.
Ewan
1
@flater Je détesterais utiliser votre sorte, le chemin le plus court, le moteur des échecs, etc. si vous le croyez. Mais je joue totalement dans votre erreur d'arrondi. Casino autorisé toute la journée.
Ewan
3
@flater démissionnez-vous lorsque vous arrivez à un jeu de fin de pion roi? Ce n'est pas parce que le jeu tout entier ne peut être forcé que de forcer. Ce n'est pas parce que vous forcez brutalement le chemin le plus court vers un réseau que vous connaissez le chemin le plus court de tous les réseaux
Ewan
2

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, un taskstableau vide ).

Après cela, vous souhaitez d’abord tester les cas les plus simples. Pour l' tasksentré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.

Toby Speight
la source
2

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é?

AnoE
la source
2

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 fftde [1,2,3,4,5]? Aucune idée. C'est quoi ifft(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:

EXPECT_TRUE(register(img1, img2).size() < min(img1.size(), img2.size()))

é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:

scale = 255
EXPECT_PIXEL_EQ_WITH_TOLERANCE(reg(img, img), img, .05*scale)

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, et task.importance > 0 pour tous task 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 tout task s, le résultat est 0 (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.

Matt Messersmith
la source
1

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.

Tobias Hagge
la source
1

Certaines des autres réponses ici sont très bonnes:

  • Tester les bases, les arêtes et les coins
  • Effectuer des contrôles de santé
  • Effectuer des tests comparatifs

... J'ajouterais quelques autres tactiques:

  • Décomposer le problème.
  • Prouver l'algorithme en dehors du code.
  • Testez que l'algorithme [éprouvé en externe] est implémenté tel que conçu.

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.

svidgen
la source
0

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.

Bill K
la source
0

Je pense qu'il est parfaitement acceptable en certaines occasions de suivre le processus:

  • concevoir un cas de test
  • utilisez votre logiciel pour obtenir la réponse
  • vérifier la réponse à la main
  • écrivez un test de régression pour que les versions futures du logiciel continuent à donner cette réponse.

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.

Michael Kay
la source
0

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:

  1. Entrées 'aléatoires'
  2. Itération sur plusieurs plages de données
  3. Construction de cas de test à partir d'ensembles de limites
  4. Tout ce qui précède.

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é.

Keith
la source