Unité testant plusieurs conditions dans une instruction IF

26

J'ai un morceau de code qui ressemble à ceci:

function bool PassesBusinessRules()
{
    bool meetsBusinessRules = false;

    if (PassesBusinessRule1 
         && PassesBusinessRule2
         && PassesBusinessRule3)
    {
         meetsBusinessRules= true;
    }

    return meetsBusinessRules;
}

Je pense qu'il devrait y avoir quatre tests unitaires pour cette fonction particulière. Trois pour tester chacune des conditions dans l'instruction if et s'assurer qu'elle retourne false. Et un autre test qui s'assure que la fonction retourne vrai.

Question: Devrait-il y avoir à la place dix tests unitaires? Neuf qui vérifie chacun des chemins d'échec possibles. C'EST À DIRE:

  • Faux Faux Faux
  • Faux Faux Vrai
  • Faux Vrai Faux

Et ainsi de suite pour chaque combinaison possible.

Je pense que c'est exagéré, mais certains des autres membres de mon équipe ne le font pas. La façon dont je le vois est que si BusinessRule1 échoue, il devrait toujours retourner faux, peu importe si elle a été vérifiée en premier ou en dernier.

bwalk2895
la source
Le compilateur utilise-t-il une évaluation gourmande pour l'opérateur &&?
suszterpatt
12
Si vous avez écrit 10 tests unitaires, vous testeriez l'opérateur &&, pas vos méthodes.
Mert Akcakaya
2
N'y aurait-il pas seulement huit tests si vous testiez toutes les combinaisons possibles? Trois paramètres booléens activés ou désactivés.
Kris Harper
3
@Mert: Seulement si vous pouvez garantir que le && sera toujours là.
Misko
Hickey: Si nous le passons à écrire des tests, c'est le temps que nous ne passons pas à faire autre chose. Chacun de nous doit évaluer la meilleure façon de passer son temps afin de maximiser nos résultats, à la fois en quantité et en qualité. Si les gens pensent que passer cinquante pour cent de leur temps à passer des tests maximise leurs résultats, ça leur va. Je suis sûr que ce n'est pas vrai pour moi - je préfère passer ce temps à penser à mon problème. Je suis certain que cela produit pour moi de meilleures solutions, avec moins de défauts, que toute autre utilisation de mon temps. Une mauvaise conception avec une suite de tests complète est toujours une mauvaise conception.
Job

Réponses:

29

Officiellement, ces types de couverture ont des noms.

Tout d'abord, il existe une couverture de prédicat : vous voulez avoir un scénario de test qui rend l'instruction if vraie et une qui la rend fausse. Le respect de cette couverture est probablement une exigence de base pour une bonne suite de tests.

Ensuite, il y a la couverture des conditions : ici, vous voulez tester que chaque sous-condition dans le if a la valeur true et false. Cela crée évidemment plus de tests, mais il détecte généralement plus de bogues, donc c'est souvent une bonne idée d'inclure dans votre suite de tests si vous en avez le temps.

Les critères de couverture les plus avancés sont généralement appelés couverture des conditions combinatoires : ici, l'objectif est d'avoir un cas de test qui passe par toutes les combinaisons possibles de valeurs booléennes dans votre test.

Est-ce mieux qu'une simple couverture de prédicat ou de condition? En termes de couverture, bien sûr. Mais ce n'est pas gratuit. Il s'agit d'un coût très élevé pour la maintenance des tests. Pour cette raison, la plupart des gens ne se soucient pas d'une couverture combinatoire complète. En général, tester toutes les branches (ou toutes les conditions) sera suffisant pour détecter les bugs. L'ajout de tests supplémentaires pour les tests combinatoires n'attrapera généralement pas plus de bugs, mais nécessite beaucoup d'efforts pour créer et maintenir. L'effort supplémentaire fait généralement que cela ne vaut pas le très petit gain, donc je ne recommanderais pas cela.

Une partie de cette décision devrait être basée sur le degré de risque que vous pensez que ce code sera. S'il a beaucoup de place pour échouer, cela vaut la peine d'être testé. S'il est quelque peu stable et ne changera pas grand-chose, vous devriez envisager de concentrer vos efforts de test ailleurs.

Oleksi
la source
2
Si les valeurs booléennes sont transmises à partir de sources externes (ce qui signifie qu'elles ne sont pas toujours validées), une couverture conditionnelle combinatoire est souvent nécessaire. Faites d'abord un tableau des combinaisons. Ensuite, pour chaque entrée, décidez si cette entrée représente un cas d'utilisation significatif. Sinon, il devrait y avoir du code quelque part (soit des assertions logicielles, soit une clause de validation) pour empêcher l'exécution de cette combinaison. Il est important de ne pas regrouper tous les paramètres dans un seul test combinatoire: essayez de partitionner les paramètres en groupes qui interagissent les uns avec les autres, c'est-à-dire qui partagent la même expression booléenne.
rwong
Êtes-vous certain des termes en gras? Votre réponse semble être la seule occurrence de «couverture condition combinée», et certaines ressources disent que «couverture prédicat» et «couverture conditionnelle» sont la même chose.
Stijn
8

En fin de compte, cela dépend de vous (équipe r), du code et de l'environnement spécifique du projet. Il n'y a pas de règle universelle. Vous (l'équipe r) devez écrire autant de tests que vous le souhaitez pour vous assurer que le code est bien correct . Donc, si vos coéquipiers ne sont pas convaincus par 4 tests, vous en aurez peut-être besoin de plus.

OTOH temps pour écrire des tests unitaires est généralement une ressource rare. Essayez donc de trouver la meilleure façon de passer le temps limité dont vous disposez . Par exemple, si vous avez une autre méthode importante avec une couverture de 0%, il peut être préférable d'écrire quelques tests unitaires pour couvrir celle-ci, plutôt que d'ajouter des tests supplémentaires pour cette méthode. Bien sûr, cela dépend aussi de la fragilité de la mise en œuvre de chacun. La planification de nombreux changements à cette méthode particulière dans un avenir prévisible peut justifier une couverture supplémentaire des tests unitaires. Il en va de même pour le chemin critique à l'intérieur du programme. Ce sont tous des facteurs que vous seul (l'équipe r) pouvez évaluer.

Personnellement, je serais généralement satisfait des 4 tests que vous décrivez, à savoir:

  • vrai faux faux
  • faux vrai faux
  • faux faux vrai
  • vrai vrai vrai

plus peut-être un:

  • vrai vrai faux

pour s'assurer que la seule façon d'obtenir une valeur de retour trueest de satisfaire aux 3 règles de gestion. Mais au final, si vos coéquipiers insistent pour que les chemins combinatoires soient couverts, il peut être moins cher d'ajouter ces tests supplémentaires que de continuer l'argument beaucoup plus longtemps :-)

Péter Török
la source
3

Si vous voulez être sûr, vous aurez besoin de huit tests unitaires en utilisant les conditions représentées par une table de vérité à trois variables ( http://teach.valdosta.edu/plmoch/MATH4161/Spring%202004/and_or_if_files/image006.gif ).

Vous ne pouvez jamais être sûr que la logique métier stipulera toujours que les vérifications sont effectuées dans cet ordre et vous voulez que le test connaisse le moins possible la mise en œuvre réelle.

smp7d
la source
2
Les tests unitaires sont des tests en boîte blanche.
Péter Török
Eh bien, l'ordre ne devrait pas avoir d'importance, && est communicatif, ou du moins devrait l'être
Zachary K
2

Oui, il devrait y avoir la combinaison complète dans un monde idéal.

Lorsque vous effectuez le test unitaire, vous devez vraiment essayer d'ignorer comment la méthode fait son travail. Fournissez simplement les 3 entrées et vérifiez que la sortie est correcte.

Telastyn
la source
1
Les tests unitaires sont des tests en boîte blanche. Et nous ne vivons pas dans un monde idéal.
Péter Török
@ PéterTörök - Nous ne vivons pas dans un monde idéal, bien sûr, mais stackexchange n'est pas d'accord avec vous sur l'autre point. Surtout pour TDD, les tests sont écrits selon les spécifications, pas l'implémentation. Personnellement, je prends la «spécification» pour inclure toutes les entrées (y compris les variables membres) et toutes les sorties (y compris les effets secondaires).
Telastyn
1
Ce n'est qu'un thread spécifique sur StackOverflow, sur un cas spécifique, qui ne devrait pas être trop généralisé. D'autant plus que ce billet actuel concerne évidemment le test de code déjà écrit.
Péter Török
1

L'État est mauvais. La fonction suivante n'a pas besoin d'un test unitaire car elle n'a pas d'effets secondaires et il est bien compris ce qu'elle fait et ce qu'elle ne fait pas. Pourquoi le tester? Ne fais-tu pas confiance à ton propre cerveau ??? Les fonctions statiques sont super!

static function bool Foo(bool a, bool b, bool c)
{
    return a && b && c;
}
Emploi
la source
2
Non, je ne fais pas confiance à mon propre cerveau - j'ai appris la manière difficile de toujours vérifier ce que je fais :-) Donc, j'aurais encore besoin de tests unitaires pour m'assurer que je n'ai par exemple rien mal tapé, et que personne ne va pour briser le code à l'avenir. Et plus de tests unitaires pour vérifier la méthode de l'appelant qui calcule l'état représenté par a, bet c. Vous pouvez déplacer la logique métier comme vous le souhaitez, au final, vous devez toujours la tester quelque part.
Péter Török
@ Péter Török, vous pouvez aussi faire des fautes de frappe dans vos tests et ainsi vous retrouver avec des faux positifs, alors où vous arrêtez? Écrivez-vous des tests unitaires pour vos tests unitaires? Je ne fais pas confiance à mon cerveau à 100% non plus, mais au bout du compte, écrire du code est ce que je fais dans la vie. Il est possible d'avoir un bogue à l'intérieur de cette fonction, mais il est important d'écrire du code de manière à ce qu'un bogue soit facile à retracer jusqu'à la source et de sorte qu'une fois que vous aurez isolé le problème et apporté une correction, vous ferez mieux . Un code bien écrit peut s'appuyer sur des tests d'intégration, principalement infoq.com/presentations/Simple-Made-Easy
Job
2
En effet, les tests peuvent également être défectueux. (TDD résout ce problème en faisant d'abord échouer les tests.) Cependant, faire le même genre d'erreur deux fois (et l'oublier) a une probabilité beaucoup plus faible. En général, aucune quantité et aucun type de test ne peut prouver que le logiciel est exempt de bogues, il suffit de réduire la probabilité de bogues à un niveau acceptable. Et en ce qui concerne la traçabilité des bogues à la source, rien ne peut battre les tests unitaires - Rulez de rétroaction rapide :-)
Péter Török
"La fonction suivante n'a pas besoin d'un test unitaire" Je pense que vous êtes sarcastique ici, mais ce n'est pas clair. Ai-je confiance en mon propre cerveau? NON! Ai-je confiance dans le cerveau du prochain gars qui touche le code? ENCORE PLUS NON! Ai-je confiance que toutes les hypothèses derrière le code seront vraies dans un an? ... vous obtenez ma dérive. De plus, les fonctions statiques tuent OO ... si vous voulez faire FP, alors utilisez un langage FP.
Rob
1

Je sais que cette question est assez ancienne. Mais je veux donner une autre perspective au problème.

Tout d'abord, vos tests unitaires devraient avoir deux objectifs:

  1. Créez de la documentation pour vous et vos coéquipiers, afin qu'après un certain temps, vous puissiez lire le test unitaire et vous assurer que vous comprenez what's the class' intentionethow the class is doing its work
  2. Pendant le développement, le test unitaire s'assure que le code que nous écrivons fait son travail comme il était prévu dans notre esprit.

Donc, en récapitulant le problème, nous voulons tester a complex if statement, pour l'exemple donné, il y a 2 ^ 3 possibilités, c'est une quantité importante de tests que nous pouvons écrire.

  • Vous pouvez vous adapter à ce fait et noter 8 tests ou utiliser des tests paramétrés
  • Vous pouvez également suivre les autres réponses et vous rappeler que les tests doivent être clairs avec l'intention, de cette façon, nous n'allons pas jouer avec trop de détails qui, dans un proche avenir, peuvent être plus difficiles à comprendre what is doing the code

En revanche, si vous êtes dans la position que vos tests sont encore plus complexes que l'implémentation, c'est parce que l'implémentation doit être repensée (plus ou moins selon les cas) plutôt que le test lui-même.

Pour les instructions if complexes, par exemple, vous pouvez penser au modèle de responsabilité de chaîne , implémentant chaque gestionnaire de cette façon:

If some simple business rule apply, derive to the next handler

Dans quelle mesure serait-il simple de tester diverses règles simples au lieu d'une règle complexe?

J'espère que ça aide,

cnexans
la source
0

C'est l'un de ces cas où quelque chose comme quickcheck ( http://en.wikipedia.org/wiki/QuickCheck ) sera votre ami. Au lieu d'écrire tous les N cas à la main, l'ordinateur doit générer tous (ou au moins un grand nombre) de cas de test possibles et valider que tous renvoient un résultat sensible.

Nous programmons des ordinateurs pour vivre ici, pourquoi ne pas programmer l'ordinateur pour générer vos cas de test pour vous?

Zachary K
la source
0

Vous pouvez transformer les conditions en conditions de garde:

if (! PassesBusinessRule1) {
    return false;
}

if (! PassesBusinessRule2) {
    return false;
}

if (! PassesBusinessRule3) {
    return false;
}

Je ne pense pas que cela réduit le nombre de cas, mais mon expérience est qu'il est plus facile de les répartir de cette façon.

(Notez que je suis un grand fan de "point de sortie unique", mais je fais une exception pour les conditions de garde. Mais il existe d'autres façons de structurer le code afin que vous n'ayez pas de retours séparés.)

Rob
la source