Bien souvent, on s'attend à ce que les tests passent. Notre outil nous présente un rassurant "Tous les tests sont passés avec succès" ou quelque chose du genre. Idéal une veille de livraison.
Quid d'un test qui nous ferait toujours ce plaisir ?
Testons une méthode de vérification de PIN. verifyPIN retourne
true lorsque le PIN de 4 chiffres passé en paramètre est correct,
false sinon. Un test négatif est recommandé, qu'en
dites-vous ? Dans cette version simplifiée, on prend la bonne valeur du
PIN. On en modifie l'un des chiffres, puis on soumet cette valeur altérée à
verifyPIN en s'attendant à ce qu'elle retourne false.
On teste chaque position du PIN (le premier chiffre, le second, etc.) afin de
s'assurer que chacune est impliquée dans la vérification :
// Testons toutes les positions d'un PIN à 4 chiffres
for (int i = 0; i < 4; i++) {
// On récupère le vrai PIN
pin = getPIN();
// On le modifie
pin[i] = (pin[i] + 1) % 10; // 0 -> 1, 1 -> 2, ..., 9 -> 0
// A partir de maintenant, pin n'est plus correct
// L'authentication devrait échouer
assertFalse(verifyPIN(pin));
}
Le test passe. Bonne nouvelle pour la livraison de demain. En fait, ce n'est pas une bonne nouvelle, par la combinaison de deux hasards :
- Pour une raison ou pour une autre,
getPINne retourne pas la bonne valeur du PIN. Un problème dans la configuration du test peut-être ? - Il se trouve que
verifyPINest buggué.verifyPINne vérifie pas le dernier chiffre du PIN (une faute de frappe dans la longueur de la comparaison ?). Par conséquent, si le PIN est "1234",verifyPINaccepte naturellement "1234", mais aussi "1235" et "1239". Ouch.
Ce bug est critique et pourtant il n'est pas détecté par notre test qui est
pourtant conçu pour cela. Que s'est-il passé ? Le but du test est de
soumettre un PIN avec un seul chiffre incorrect. Comme getPIN ne
retourne pas la bonne valeur, il y a toujours des erreurs sur les trois
premiers chiffres que verifyPIN détecte, peu importe que
quatrième. Ainsi que le test l'attend, l'authentification échoue, mais pas pour
la bonne raison.
Le bug ne sera pas détecté et le produit sera livré demain comme prévu. Un jour, quelqu'un se rendra compte de l'erreur et là...
Donnons-lui une chance d'échouer
Il existe plusieurs moyens de rendre un test résistant. Pour un test négatif de ce genre, une méthode classique est d'ajouter un cas positif :
pin = getPIN();
// Avec le bon PIN, l'authentification doit réussir
assertTrue(verifyPIN(pin));
// Testons toutes les positions d'un PIN à 4 chiffres
for (int i = 0; i < 4; i++) {
... // Pas d'autre changement
}
Le test est plus sûr. Désormais, lorsque getPIN retourne une
mauvaise valeur, le test échoue immédiatement puisque verifyPIN
indique d'emblée que le PIN est incorrect. Certes, le test sortira failed pour
une mauvaise raison (une bonne raison étant un bug de code), mais au moins
l'erreur ne passera pas inaperçue et le testeur fera le nécessaire pour la
corriger. Au pire, on livrera après-demain.
Comment ça "paranoïaque" ?
La vérification du PIN en début de test réduit considérablement le risque évoqué ci-dessus. Toute personne raisonnable s'arrêterait là.
Cependant, "raisonnable" est tout juste un minimum pour nous testeurs. D'un côté, nous traquons les bugs, sortons des sentiers battus pour trouver les scénarios litigieux, cherchons les failles... De l'autre, nous écrivons des tests et prenons le risque de commettre exactement les même erreurs que celles que nous recherchons. Et dans notre cas, personne n'est là pour tester notre travail. Nous devons nous efforcer de trouver nos propres erreurs. Pas simple.
Pour reprendre l'exemple du PIN, on peut rendre le test encore plus sûr :
// Testons toutes les positions d'un PIN à 4 chiffres
for (int i = 0; i < 4; i++) {
// On récupère le vrai PIN
pin = getPIN();
// Avec le bon PIN, l'authentification doit réussir
assertTrue(verifyPIN(pin));
// On le modifie
pin[i] = (pin[i] + 1) % 10; // 0 -> 1, 1 -> 2, ..., 9 -> 0
// A partir de maintenant, pin n'est plus correct
// L'authentication devrait échouer
assertFalse(verifyPIN(pin));
}
La valeur de getPIN est maintenant vérifiée à chaque itération.
En quoi est-ce plus sûr ? A moins d'une erreur de ma part dans le code qui
précède, cela n'apporte rien. Mais nous ne cherchons pas à éviter les erreurs
que nous connaissons : lorsqu'on a conscience d'une erreur, on la corrige,
tout simplement. Il s'agit plutôt d'anticiper des problèmes typiques. Comme
dans cet exemple :
// On récupère le vrai PIN
pin = getPIN();
// Testons toutes les positions d'un PIN à 4 chiffres
for (int i = 0; i < 4; i++) {
// Avec le bon PIN, l'authentification doit réussir
assertTrue(verifyPIN(pin));
// On le modifie
pin[i] = (pin[i] + 1) % 10; // 0 -> 1, 1 -> 2, ..., 9 -> 0
// A partir de maintenant, pin n'est plus correct
// L'authentication devrait échouer
assertFalse(verifyPIN(pin));
}
L'appel à getPIN est maintenant sorti de la boucle
for. Après tout, il n'est pas utile de l'appeler quatre fois
n'est-ce pas ? En réalité, oui, c'est indispensable.
Le test veut soumettre à verifyPIN un PIN dont tous les
chiffres sont corrects sauf un. Or, en l'état, ce n'est pas ce que fait le
test. En effet, puisque pin n'est plus réinitialisé en début de
boucle for, les irrégularités introduites dans pin
s'accumulent d'une itération à l'autre. A la quatrième itération, ce n'est pas
seulement le dernier mais tous les chiffres de pin qui sont incorrects. Une
fois de plus, le bug ne serait pas découvert... si la vérification de
pin n'était pas faite en début de boucle. Grâce à elle, lors de la
seconde itération, le test échouera en constatant que
verifyPIN(pin) retourne false.
Les self-tests ne sont pas toujours facile à introduire. On pourrait
également s'interroger sur les éventuels effets de bord introduits par les
appels supplémentaires à verifyPIN. C'est néanmoins une technique
à garder à l'esprit, spécialement dans le cadre de tests négatifs.
