Un test pour place_order a besoin d’une passerelle de paiement, d’un client SMS et d’un calculateur de taxes. Branchez les vraies dépendances, et chaque exécution de test débite une carte et envoie un SMS. Un test unitaire doit s’exécuter en quelques millisecondes, sans réseau : les trois doivent donc être remplacées par autre chose. Cet « autre chose » existe en cinq variantes, chacune résolvant un problème différent, et les traiter comme interchangeables est la raison pour laquelle les débats « mets juste un mock » en revue de code tournent en rond.
Qu’est-ce qu’un test double
Gerard Meszaros a inventé le terme « test double » dans xUnit Test Patterns comme terme générique pour tout ce qui remplace une dépendance réelle pendant un test, à la manière d’une doublure qui remplace une actrice. Sur un tournage, on utilise des doublures différentes selon les plans : l’une n’a qu’à se tenir en arrière-plan avec la bonne coiffure, l’autre fait vraiment l’accident de voiture. Les test doubles se répartissent de la même façon. Dummy, stub, spy, fake et mock sont cinq rôles différents, et ce qui les distingue, c’est ce que le test vérifie, pas la sophistication de l’objet de substitution.
Voici la classe que testent tous les exemples ci-dessous. Trois de ses dépendances valent la peine d’être remplacées ; la quatrième n’a aucune importance :
| |
logger n’intervient que sur le chemin d’erreur. Cela compte pour le premier exemple.
Deux petites doublures sont réutilisées dans les tests suivants ; elles sont donc définies une seule fois ici :
| |
Qu’est-ce qu’un objet dummy dans les tests
Un dummy n’est passé que pour satisfaire un paramètre que le test n’exerce jamais. Il ne fait rien et ne vérifie rien. Le test du chemin heureux ci-dessous n’atteint jamais la branche d’exception, donc logger peut être n’importe quoi qui survit à une affectation d’attribut :
| |
object() fonctionne parce que rien n’appelle de méthode sur lui. Si un changement ultérieur faisait aussi logguer place_order sur le chemin de succès, ce test échouerait avec une AttributeError. Cet échec est utile : il indique que le dummy a cessé d’être un dummy.
Qu’est-ce qu’un stub dans un test unitaire
Un stub renvoie une réponse préfabriquée pour que le reste du test reste déterministe. Le vrai calculateur de taxes pourrait appeler une API de taux de taxe. Le test ne veut pas de ça, donc StubTaxCalculator renvoie 108.00 quel que soit le sous-total reçu. Vous l’avez déjà vu à l’œuvre dans le test du dummy ci-dessus, avec StubGateway qui fait le même travail pour charge.
Rien ne vérifie si total_with_tax a été appelé, ni avec quel argument. Un stub fournit au code testé une entrée contrôlée ; il ne vérifie jamais une interaction. C’est la frontière entre un stub et les trois doublures suivantes.
Qu’est-ce qu’un test spy
Un spy enregistre les appels qu’il reçoit afin que le test puisse les examiner après l’exécution du code testé. unittest.mock.Mock enregistre automatiquement chaque appel, donc la plupart des suites Python l’utilisent comme spy même quand le mot n’apparaît jamais dans le test :
| |
Rien n’a été configuré sur sms_spy avant l’appel. Il a accepté ce qu’on lui a donné, l’a enregistré, et le test l’a interrogé après coup.
Qu’est-ce qu’un objet fake dans les tests
Un stub ne peut pas vous dire si deux débits consécutifs obtiennent deux identifiants différents, car il ne connaît qu’une seule valeur. Quand un test a besoin d’un comportement sur plusieurs appels, c’est un fake qu’il faut : une petite implémentation qui fonctionne vraiment.
| |
| |
FakeGateway ne fait aucun appel réseau et n’a aucun processeur de paiement derrière lui, mais il conserve un état comme le ferait le vrai. Le test avait justement besoin de cet état, et un stub n’aurait pas pu le fournir.
Qu’est-ce qu’un objet mock
Dans la définition de Meszaros, un mock porte ses attentes dans le test : les règles d’un appel valide sont fixées avant l’exécution du code, et un appel qui les enfreint échoue immédiatement. unittest.mock est plus souple que ça, puisque l’assertion se fait toujours après coup, mais create_autospec vous donne la partie qui compte. Il construit la doublure à partir d’une vraie classe, donc chaque appel est vérifié par rapport aux vraies signatures de méthode au moment où il se produit :
| |
Changez place_order pour appeler self.gateway.charge(total, currency="usd"), et ce test échoue avec une TypeError à l’intérieur de place_order, parce que Gateway.charge n’accepte pas d’argument currency. Imaginez maintenant un simple Mock() au même endroit, avec quelqu’un qui met à jour l’assertion pour correspondre au nouvel appel. Le test passe, et le code casse dès qu’il rencontre la vraie passerelle. Un spy confirme qu’un appel a eu lieu. Un mock autospécifié confirme en plus que cet appel est bien un appel que le véritable objet accepterait.
Mock vs stub vs spy vs fake : tableau comparatif
| Doublure | Ce qu’elle fait | À utiliser quand |
|---|---|---|
| Dummy | Passé pour satisfaire un paramètre ; jamais appelé | Une dépendance existe uniquement parce que la signature l’exige sur ce chemin |
| Stub | Renvoie toujours la même réponse préfabriquée | Le test a besoin d’une valeur fixe et déterministe en sortie d’une dépendance |
| Spy | Enregistre les appels pour que le test puisse les examiner après coup | Le test doit confirmer qu’une interaction a eu lieu, sans la configurer au préalable |
| Fake | Une implémentation réduite mais fonctionnelle de l’objet réel | Le test a besoin d’un comportement réel sur plusieurs appels, pas d’une seule valeur préfabriquée |
| Mock | Construit avec des attentes sur les appels valides, vérifiées au moment où ils se produisent | Le test doit vérifier la forme exacte d’un appel : arguments, nombre, ordre |
gateway a été un stub, un fake et un mock dans trois tests de la même classe. La doublure suit ce que le test vérifie, pas la dépendance.
Quand les test doubles rendent les tests pires
Mocker chaque collaborateur couple le test à la façon dont le code est écrit plutôt qu’à ce qu’il fait. Ajoutez un test qui vérifie que total_with_tax est appelé exactement une fois, puis modifiez place_order pour l’appeler une seconde fois à des fins de logging : le test casse, alors que rien de ce qui compte pour un utilisateur n’a changé. Le test avec le fake ci-dessus n’a pas ce problème, car il vérifie le résultat (deux identifiants de débit distincts, les bons montants enregistrés) plutôt que la forme de l’appel.
Mocker une bibliothèque que vous ne possédez pas pose le même problème, un niveau plus haut. Autospécifiez directement un client tiers, et chaque test qui le touche dépend des signatures de méthode de cette bibliothèque ; une montée de version mineure qui renomme un paramètre casse des tests dans toute la suite. Enveloppez plutôt la bibliothèque dans votre propre petite interface, et doublez celle-ci. Quand la bibliothèque change, seul le wrapper change.
Parfois, le bon nombre de doublures est zéro. Si la vraie dépendance est déjà rapide et sans effet de bord, comme une fonction pure ou une connexion SQLite en mémoire, la doubler ajoute de l’indirection sans rien attraper. Les doublures sont faites pour les dépendances lentes, distantes, non déterministes, ou qui ont des effets de bord que vous ne voulez pas dans une exécution de test.
Comment choisir le bon test double
Partez de ce que le test vérifie, pas de ce qui semble le plus sûr à simuler. Si vous avez besoin d’une entrée prévisible pour qu’une assertion ultérieure tienne, stubbez-la. Pour confirmer que le SMS est parti, espionnez-le. Si la dépendance doit se comporter correctement sur plusieurs appels, écrivez un fake une seule fois et réutilisez-le partout où cette dépendance apparaît. Et quand un appelant qui passe de mauvais arguments doit faire échouer le test avant qu’il n’échoue en production, utilisez un mock autospécifié.
Rien de tout cela ne fonctionne sur une classe qui construit elle-même ses dépendances en interne, car il n’y a nulle part où glisser une doublure. C’est le problème que résout la dependency injection, et c’est la première chose à corriger si une classe résiste à toutes les doublures de cet article. Une fois la couture en place, décidez quelle doublure appartient à quel niveau de test. Si vous travaillez en test-first, vous aurez recours aux doublures tôt, puisque le test existe avant qu’il y ait du vrai code à appeler.