EasyMock: Mythes et réalités

le 01/11/2010 par Henri Tremblay
Tags: Software Engineering

Il y a eu beaucoup de discussions sur le web dernièrement. À propos du meilleur framework de "mock". EasyMock et Mockito sont fréquemment comparés. En particulier parce que Mockito est très inspiré par EasyMock (et utilise une partie du code technique) mais a sa propre syntaxe. Malheureusement, en lisant certaines comparaisons, j'ai noté certaines choses tout simplement fausses. Cet article a pour but de rétablir la vérité ainsi que de vous donner mon opinion sur les deux frameworks. Étant le développeur principal d'EasyMock, je suis bien sûr biaisé. Je pense toutefois avoir fait de mon mieux pour vous fournir une honnête comparaison.

EasyMock ne mock pas de classes, uniquement des interfaces

C'est faux. Depuis EasyMock 3, la version classique d'EasyMock sait mocker des classes. Cela était vrai auparavant pour des raisons historiques. Il y avait en effet EasyMock et EasyMock Class Extension. La Class Extension ne continue maintenant d'exister que pour des raisons de rétrocompatibilité (et est désormais une coquille vide déléguant à EasyMock).

Le code d'EasyMock est plus longuet, car il faut appeler replay()

Sincèrement... Comme si appeler une méthode de plus pourrait tuer la productivité... Et j'ai toujours pensé que son appel faisait une jolie séparation entre la phase de préparation et celle d'exécution. Je me dois de concéder une chose toutefois. Il était relativement rébarbatif d'avoir à appeler replay sur chaque mock (replay(mock1, mock2, mock3)). C'est pour cela qu'EasyMockSupport a été introduite. Cette classe permet d'appeler replayAll() pour passer d'un seul coup tous les mocks en mode exécution.

De plus, l'argument originel est un peu fallacieux. Mockito demande d'écrire plusieurs verify qui sont en fait inclus dans les expect d'EasyMock. Voici un exemple*:

EasyMock (avec EasyMockSupport)

List mock = createNiceMock(List.class);

// Le code métier attend ceci
expect(mock.get(0)).andStubReturn("one");
expect(mock.get(1)).andStubReturn("two");
mock.clear();

replayAll();

// L'actuel code métier
someCodeThatInteractsWithMock(mock);

// Vérifier que tout ce qui devait être appelé l'a été
verifyAll();

Mockito

List mock = mock(List.class);

// Le code métier attend ceci
when(mock.get(0)).thenReturn("one");
when(mock.get(1)).thenReturn("two");

// L'actuel code métier
someCodeThatInteractsWithMock(mock);

// Vérifier que tout ce qui devait être appelé l'a été
verify(mock).get(0);
verify(mock).get(1);
verify(mock).clear();

On ne peut pas "espionner" avec EasyMock

Mockito possède une sympathique fonctionnalité permettant d'espionner. Tu crées un vrai objet (pas un mock) et tu l'espionnes. En gros, ça signifie que l'objet va se comporter comme d'habitude, mais que tous les appels seront enregistrés pour permettre de les vérifier ultérieurement. Voici un exemple:

List list = new LinkedList(); // vrai objet
   List spy = spy(list); // enveloppe d'espionnage

   //optionnellement, on peut "stubber" certaines méthodes:
   when(spy.size()).thenReturn(100);

   // cet appel est espionné
   spy.add("one");

   // étant donné qu'un vrai appel à add() a été fait, get(0) va retourner "one"
   assertEquals("one", spy.get(0));

   // La méthode size() a été "stubber" et va donc retourner 100
   assertEquals(100, spy.size());

   //optionnellement, on peut vérifier que l'appel à add() a été fait comme prévu
   verify(spy).add("one");

Il n'y a pas de fonctionnalité d'espionnage en tant que tel dans EasyMock. Toutefois, j'ai essayé de penser aux cas où j'en aurai besoin et j'en suis venu à la conclusion qu'un mélange des fonctionnalités de capture, de mock partiel et de délégation devrait permettre de nous en sortir. Par contre, c'est potentiellement un peu plus laborieux. Mais je pense que les cas d'utilisation sont plutôt rares. Je ne suis pas contre ajouter une fonction d'espionnage si j'ai tort.

À propos de l'exemple ci-dessus, nous devons penser au but de ce test. Imaginons que dans ce cas, nous avons du vieux code pour lequel nous voulons vérifier que add est correctement appelée mais sans la mocker. Vous conviendrez que c'est un cas rare, mais possible. Nous pouvons faire ceci:

// Vrai objet
    List real = new LinkedList();
    // Création du mock
    List list = createMock(List.class);

    // Espionner le paramètre, mais conserver le vrai comportement
    Capture c = new Capture();
    expect(list.add(capture(c))).andDelegateTo(real);

    replayAll();

    // le test en tant que tel
    list.add("one");

    // get() va retourne "one" comme prévu
    assertEquals("one", real.get(0));

    // vérifier la capture (qui retourne une exception si rien n'a été capturé)
    assertEquals("one", c.getValue());

    // il n'est pas nécessaire d'appeler verify car vérifier la capture est suffisant
    verifyAll();

Meilleure gestion du void par Mockito

Le problème avec les méthodes void c'est qu'elles ne retournent rien. On ne peut par faire ceci expect(myVoidMethod()), car ça ne compilera pas. EasyMock et Mockito ne vont tous les deux pas vous demander "d'expecter" quelque chose car de toute façon, la plupart du temps on ne veut rien retourner. On fera donc

mock.myVoid();

C'est incorrect de penser qu'il est nécessaire d'appeler expectLastCall() avec EasyMock. Ce n'est toutefois pas interdit et cela peut rendre plus évident le fait que vous enregistrez un appel. Il est tout autant inutile d'appeler once() car EasyMock s'attend à un seul appel par défaut.

// suffisant pour enregistrer un appel à myVoid
   mock.myVoid();
  // aucun besoin d'ajouter cette ligne
  expectLastCall().once();

La différence principale est dans la syntaxe pour faire "retourner" quelque chose à une méthode void. Pour lancer une exception par exemple. Mockito fera

doThrow(new RuntimeException()).when(mockedList).clear();

ce qui est l'inverse de sa syntaxe habituelle. EasyMock fera plutôt

mockedList.clear();
expectLastCall().andThrow(new RuntimeException());

Une ligne de plus, mais dans l'ordre habituel (le retour est après l'appel).

Comme vous voyez, c'est blanc bonnet et bonnet blanc et dans les deux cas, dicté par une nécessité technique.

Les erreurs sont plus claires dans Mockito

Cela fut malheureusement vrai pendant quelque temps. Beaucoup d'efforts ont été mis dans EasyMock 3 pour rectifier le tir. J'espère avoir réussi.

Les tests d'EasyMock brisent plus souvent

Oui. C'est mieux comme ça. Ou c'est en tout cas mon point de vue. C'est vraiment une des différences principales. Avec EasyMock, vous êtes obligés de tout enregistrer. Ce qui fait que le test brisera dès que vous changez le code métier. Avec Mockito, vous allez vous assurer d'un certain nombre de comportements et seuls ceux-ci peuvent briser.

Quelle est la différence? EasyMock vous force à tout enregistrer. Vos tests vont ensuite briser et vous serez forcé d’aller y jeter un coup d'oeil pour vous demander si c'est normal. Mockito ne vous force pas de cette façon. Le test ne vérifiera que ce que le développeur originel a pensé à tester. Si vous n'allez pas voir l'ancien test, vous ne saurez pas qu'une vérification n'a pas été faite. Le test restera "vert" et laissera passer de sournois bogues. Pour moi, c'est vraiment la différence principale. Un compromis entre des tests plus rapides à écrire et des tests qui testeront l'imprévu. Voyons un exemple pour bien comprendre.

Mockito

//création du mock
 List mockedList = mock(List.class);

 // appel du mock dans du code métier
 mockedList.add("one"); // la valeur de retour n'est pas spécifiée ni utilisée
 // mockedList.clear(); // ajouter cet appel ne brisera PAS le test

 // vérification que add() a été appelé avec le bon paramètre
 verify(mockedList).add("one");

EasyMock

//création du mock
 List mockedList = createMock(List.class);
 // Nous sommes forcés de retourner une valeur ayant un sens
 expect(mockedList.add("one").andReturn(true);
 replayAll();

 // appel du mock dans du code métier
 mockedList.add("one"); // la valeur de retour n'est pas utilisé a été spécifiée
 // mockedList.clear(); // ajouter cet appel au code métier brisera le test

 // vérification que add() a été appelé avec le bon paramètre
 // et qu'aucune autre méthode n'a été appelée
 verifyAll();

L'exemple utilisant EasyMock teste ce qui est prévu, mais fait deux choses supplémentaires.

  1. S'assure que le mock imite correctement une List en forçant une valeur de retour
  2. Empêche le test de fonctionner si un autre appel au mock est fait par le code métier. Ceci vous forcera à vous demander s'il est normal que le test ne fonctionne plus suite à la modification du code métier.

Malgré tout, je vais quand même vous donner quelques trucs pour que vos tests tiennent un peu mieux. Pensez à ce qui est vraiment important. Est-ce important que cet accesseur ne soit appelé qu'une seule fois? Non? Faites-en un stub. Vous souciez-vous de la valeur de ce paramètre? Non? Utilisez anyObject comme matcher. Ça semble idiot, mais c'est la meilleure méthode pour avoir des tests robustes. C'est une erreur classique. Provenant du fait qu'on se bat avec le test pour s'aligner avec l'implémentation de la méthode testée au lieu de tester ce que la méthode est censée faire. (Entends-je TDD au loin?)

Ce que je pense des développeurs de Mockito

Certains d'entre vous pensent peut-être que je les déteste. Ils ont pris mon code et mes utilisateurs. Heureusement, pendant qu'on fait des procès à tour de bras dans d'autres sphères, ça ne fonctionne pas comme ça dans le monde open source. Il est traditionnel d'utiliser les idées des autres et de tenter de les améliorer pour le bien de tous. L'éthique dicte toutefois d'avertir quand on le fait. Ce qu'ils ont fait. J'ai probablement été l'une des premières personnes à apprendre l'existence de Mockito. Et ils se font un point d'honneur de rendre public que beaucoup de code et d'idées proviennent d'EasyMock. Ils ont fait de très jolies choses qui m'ont fait réfléchir. Et j'aime bien leur logo. Bien sûr que nous ne vivons pas chez le Bisounours. Bien sûr qu'il y a de la compétition. Mais c'est ce qui nous fait évoluer.

Dernières pensées

Je dois vous avouer que les développements d'EasyMock ont été plus lents que je ne l'aurai voulu ces dernières années. EasyMock 3 a toutefois apporté de nombreuses améliorations. EasyMockSupport, une nouvelle API de mock de classes, une capture améliorée, la délégation, etc. Sans compter la fusion d'EasyMock et d'EasyMock Class Extension qui aurait dû avoir lieu il y a bien longtemps. De nombreuses autres améliorations sont dans les tuyaux. Je cherche même de l'aide! N'hésitez donc pas à me contacter (plus à ce sujet sur la mailing list d'EasyMock. En ligne de mire, j'aimerai entre autres une meilleure intégration avec les frameworks de test (JUnit, TestNG) et avec Hamcrest (bientôt disponible dans EasyMock 3.1). J'aimerai aussi quelques fonctionnalités puissantes permettant de vaincre les dernières limitations des mocks comme, par exemple, les méthodes finales et privées. Et toujours et encore réduire le code inutile. De plus, je ne suis aussi pas fermé à l'ajout d'une syntaxe plus Mockito style. Je ne pense pas qu'il s'agit de la meilleure approche à long terme, mais bon... à l'époque on pensait que seules les interfaces devraient être mockées...

*: Une partie des exemples sont une version modifiée des exemples du site de Mockito