Mocks & Stubs : Leurres des Tests
Ces dernières années, les tests unitaires se sont imposés comme une technique obligatoire pour assurer la qualité d’un projet de développement. Pour simplifier le travail, de nombreux frameworks ont fait leur apparition pour à peu près chaque technologie. Cependant, certains comportements d’une application sont difficiles à tester par ce biais …
Voici une liste des cas où un framework standard (de type xUnit) n’est pas forcément approprié :
- Les tests portent sur des parties du programme très dépendantes des données
Imagineons deux comportements à tester, chacun modifiant des données sémantiquement proches. Comment s’assurer qu’un test du premier comportement soit toujours valide après avoir testé l’autre comportement ? En réinitialisant les données entre chaque test ? Cela peut s’avérer très long et surtout peu sûr en terme d’intégrité, surtout si vos suites de tests sont importantes (150 tests et plus). - Le comportement à tester dépend d’un paramètre de la machine hôte ou fournit un résultat non déterministe
Ce problème est assez similaire au précédent. Que faire si le programme a besoin de la date courate, d’une variable d’environnement ? Et quand le comportement est dépendant de la présence du réseau ? - Le comportement est très dépendant d’autres composants, qui en plus peuvent être très lents à l’exécution
Comment tester seulement le comportement qui nous intéresse en le détachant de ses dépendances ?
Plusieurs solutions sont apparues au fil du temps. Cette liste n’est pas exhaustive, on pourrait par exemple y rajouter les tests de type Spy, que je ne décrirai pas ici.
Les « Dummy objects »
Des objets factices sont passés à l’appel du comportement. Le comportement n’en a pas réellement besoin et le test ne s’y intéresse pas. Ils ne servent qu’à remplir la liste des paramètres nécessaires à l’initialisation du comportement. Cette technique est très basique et ne constitue pas en soi un Pattern à proprement parler.
Les « Stubs » ( != Winstub)
Implémenter un Stub signifie remplacer un objet réel dont le système dépend par un nouveau point de contrôle, dont l’objectif est d’attendre des données très précises pour renvoyer une réponse connue.
Exemple, voici l’interface du composant à tester :
public interface MailService {
public void send (Message msg);
}Voici un exemple de Stub implémentant cette interface :
public class MailServiceStub implements MailService {
private List<Message> messages = new ArrayList<Message>();
public void send(Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
}Et un test utilisant ce Stub :
public void testMemberMailSentWhenSubscribed() {
// Création d'un membre
Member member = new Member("login", "password");
// On insère le stub à la place du mailer par défaut
MyApp.setMailer(new MailServiceStub());
// On enregistre le membre
MyApp.register(member);
// Un mail doit être envoyé
assertEquals(1, mailer.numberSent());
}Les « Fake objects »
De faux objets ayant un comportement similaire à l’objet réel, mais d’une façon simplifiée. Il n’agit pas comme un point de contrôle, mais permet d’accélérer l’exécution des tests (par exemple en remplaçant l’accès à la base de données par des données dans un tableau).
Exemple avec un DAO :
class MemberDAO {
private Connection connection;
public Member find(String id) {
return connection.createQuery("..").findOne();
}
}Et son Fake :
class FakeMemberDAO {
private Map<Long, Member> members;
public Member find(String id) {
return this.members.get(id);
}
}On remplacera le vrai DAO par son Fake un peu de la même manière que l’on injecte un Stub pour remplacer un service.
Les « Mock objects »
Un Mock agit comme un point de contrôle, un peu à la façon d’un Stub. La différence principale vient du fait qu’avec le Stub on vérifie l’état de l’objet alors qu’avec le Mock on en vérifie le comportement (les interactions), ce qui est à mon goût plus subtil.
Voici l’exemple précédent adapté pour l’utilisation des Mocks (l’interface MailService reste identique). C’est un exemple d’implémentation parmi d’autres, différente selon le framework que vous utilisez (nous les distinguerons par après) :
public void testMemberMailSentWhenSubscribed() {
// Création d'un membre
Member member = new Member("login", "password");
// On initialise un Mock dont le proxy remplacera le mailer par défaut
Mock mailer = mock(MailerService.class);
MyApp.setMailer(mailer.proxy());
// On s'attend à ce que la méthode "send" soit appelée une fois.
mailer.expects(once()).method("send");
// On enregistre le membre
MyApp.register(member);
}On remarque que le test ne comporte aucune assertion, car les assertions servent à vérifier l’état d’un objet à un moment donné, ce qui n’est pas dans la philosophie du Mock.
Différentes implémentations du Mock
Afin de mettre en place ce système, vous aurez le choix entre plusieurs implémentations. Je vais présenter rapidement quelques frameworks dans divers langages ayant chacun leur façon de répondre au problème (les implémentations ne seront pas toujours fonctionnelles, je n’ai pas pu toutes les tester, elles servent néanmoins à prouver qu’il existe toute une floppée de méthodes). Pour chaque langage, je vais essayer de m’adapter aux conventions d’écriture du code généralement utilisées (un code .Net diffère beaucoup d’un code Python !).
Aucun framework
Si vous n’avez pas la possibilité d’utiliser un framework particulier pour diverses raisons, une solution existe tout de même. J’ai découvert dans un article sur le site d’IBM une façon de faire (en Java notamment, mais cela peut sûrement s’étendre à d’autres langages). Si l’on s’en réfère aux définitions ci-dessus, c’est en fait un mélange entre la technique du Mock et la technique du Stub :
public void testMemberMailSentWhenSubscribed() {
// Création d'un membre
Member member = new Member("login", "password");
// On initialise un Mock ayant une méthode de validation
MailService mailer = new MailService() {
private List<Message> messages = new ArrayList<Message>();
public void send(Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
public void validate() {
assertEquals(1, numberSent());
}
};
// On met en place le Mock
MyApp.setMailer(mailer);
// On enregistre le membre
MyApp.register(member);
// On valide le comportement
mailer.validate();
}Nous verrons dans quelques jours de multiples frameworks dans divers langages (Java, Python, Ruby, .NET …) qui vous simplifieront la tâche par rapport à cette approche qui, si elle permet de bien comprendre le mécanisme à l’œuvre, peut être simplifiée par l’utilisation de librairies tierces.
Voici pour finir quelques références utiles :
- La définition d’un Mock Object
- La référence !
- Une explication de la différence entre les Mocks et les Stubs
- Une implémentation Java sans framework
- Une comparaison des frameworks Java
- Les différents outils de test pour Python
- Une présentation des Mock Objects en Ruby
Commentaires 3 commentaires
[...] différentes approchesL’approche par les Mocks / FakesBien que très pratiques, les Mocks peuvent ne pas être appropriés pour les tests d’accès à la base, certains les [...]
[...] publié le 22 janvier 2010 par Killian EbelAfin de compléter la série d’articles sur les tests logiciels, voici une méthode permettant de tester efficacement une application [...]

[...] la suite de mon article précédent, « Leurre des tests » … Nous allons passer rapidement en revue les différents outils à notre disposition sur [...]