Dans le monde réel, il est parfaitement normal d'écrire des tests unitaires pour le code de quelqu'un d'autre. Bien sûr, le développeur d'origine aurait déjà dû le faire, mais souvent vous recevez du code hérité là où cela n'a tout simplement pas été fait. Soit dit en passant, peu importe si ce code hérité est venu il y a des décennies d'une galaxie très éloignée, ou si l'un de vos collègues l'a vérifié la semaine dernière, ou si vous l'avez écrit aujourd'hui, le code hérité est un code sans test
Demandez-vous: pourquoi écrivons-nous des tests unitaires? Passer au vert n'est évidemment qu'un moyen de parvenir à une fin, le but ultime est de prouver ou d'infirmer des affirmations sur le code testé.
Imaginons que vous disposiez d'une méthode qui calcule la racine carrée d'un nombre à virgule flottante. En Java, l'interface le définirait comme:
public double squareRoot(double number);
Peu importe que vous ayez écrit l'implémentation ou que quelqu'un d'autre l'ait fait, vous voulez affirmer quelques propriétés de squareRoot:
- qu'il peut retourner des racines simples comme sqrt (4.0)
- qu'il peut trouver une vraie racine comme sqrt (2.0) avec une précision raisonnable
- qu'il trouve que sqrt (0,0) est 0,0
- qu'il lève une exception IllegalArgumentException lorsqu'il reçoit un nombre négatif, c'est-à-dire sur sqrt (-1.0)
Vous commencez donc à les écrire en tant que tests individuels:
@Test
public void canFindSimpleRoot() {
assertEquals(2, squareRoot(4), epsilon);
}
Oups, ce test échoue déjà:
java.lang.AssertionError: Use assertEquals(expected, actual, delta) to compare floating-point numbers
Vous avez oublié l'arithmétique à virgule flottante. OK, vous présentez double epsilon=0.01
et allez:
@Test
public void canFindSimpleRootToEpsilonPrecision() {
assertEquals(2, squareRoot(4), epsilon);
}
et ajoutez les autres tests: enfin
@Test
@ExpectedException(IllegalArgumentException.class)
public void throwsExceptionOnNegativeInput() {
assertEquals(-1, squareRoot(-1), epsilon);
}
et oups, encore une fois:
java.lang.AssertionError: expected:<-1.0> but was:<NaN>
Vous auriez dû tester:
@Test
public void returnsNaNOnNegativeInput() {
assertEquals(Double.NaN, squareRoot(-1), epsilon);
}
Qu'avons-nous fait ici? Nous avons commencé avec quelques hypothèses sur la façon dont la méthode devrait se comporter, et nous avons constaté que toutes n'étaient pas vraies. Nous avons ensuite rendu la suite de tests verte, pour noter que la méthode se comporte selon nos hypothèses corrigées. Désormais, les clients de ce code peuvent compter sur ce comportement. Si quelqu'un devait échanger l'implémentation réelle de squareRoot avec quelque chose d'autre, quelque chose qui, par exemple, a vraiment levé une exception au lieu de renvoyer NaN, nos tests le détecteraient immédiatement.
Cet exemple est trivial, mais souvent vous héritez de gros morceaux de code où il n'est pas clair ce qu'il fait réellement. Dans ce cas, il est normal de placer un faisceau de test autour du code. Commencez avec quelques hypothèses de base sur la façon dont le code doit se comporter, écrivez des tests unitaires pour eux, testez. Si vert, bon, écrivez plus de tests. Si Rouge, eh bien maintenant vous avez une assertion ratée que vous pouvez tenir contre une spécification. Il y a peut-être un bogue dans le code hérité. Peut-être que la spécification n'est pas claire sur cette entrée particulière. Vous n'avez peut-être pas de spécification. Dans ce cas, réécrivez le test de sorte qu'il documente le comportement inattendu:
@Test
public void throwsNoExceptionOnNegativeInput() {
assertNotNull(squareRoot(-1)); // Shouldn't this fail?
}
Au fil du temps, vous vous retrouvez avec un faisceau de test qui documente le comportement réel du code et devient une sorte de spécification codée. Si vous souhaitez modifier le code hérité ou le remplacer par autre chose, vous disposez du faisceau de test pour vérifier que le nouveau code se comporte de la même manière ou que le nouveau code se comporte différemment de manière attendue et contrôlée (par exemple, corrige le bug que vous attendez qu'il corrige). Ce harnais ne doit pas être complet le premier jour, en fait, avoir un harnais incomplet est presque toujours mieux que de ne pas avoir de harnais du tout. Avoir un harnais signifie que vous pouvez écrire votre code client avec plus de facilité, vous savez où vous attendre à ce que les choses se cassent lorsque vous changez quelque chose et où elles se cassent quand elles le font finalement.
Vous devriez essayer de sortir de la mentalité selon laquelle vous devez passer des tests unitaires simplement parce que vous le devez, comme vous rempliriez des champs obligatoires sur un formulaire. Et vous ne devez pas écrire de tests unitaires juste pour rendre la ligne rouge verte. Les tests unitaires ne sont pas vos ennemis, les tests unitaires sont vos amis.