Choses à faire

24h dans une journée, et tant de choses à faire !

Mocks & Stubs : Leurres des Tests

publié le 8 octobre 2009 par Killian Ebel

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 un « Mockingbird » ! (Photo par MrBobBaker)

Voici un « Mockingbird » ! (Photo par MrBobBaker)

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 :

Les textes, illustrations et démonstrations présents sur ce site sont la propriété de leurs auteurs respectifs, sauf mention contraire (photo de la bannière).
Chosesafaire.fr, un site propulsé par Wordpress, vous est proposé par Pierre Quillery & Killian Ebel.

Valid XHTML 1.0 Strict