EasyMock: Mythes et réalités

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

11 commentaires sur “EasyMock: Mythes et réalités”

  • Article très intéressant ! Pour les méthodes finales/statiques/privées, j'ai toujours utilisé powermock, que cela soit au dessus d'easymock et de mockito. J'aime également chez mockito la structure en given/when/then que l'on retrouve de plus en plus souvent au sein des frameworks de tests et le fait que les 'mocks' sont lénients de base. Tout à fait d'accord avec le anyObject et le stub et dommage que le premier mock de la doc easymock ne soit pas fait avec un exemple stub :)
  • Utilisateur d'EasyMock j'en profite pour te remercier de maintenir ce framework. Pour avoir utilisé Mockito et EasyMock, je préfère de loin la syntaxe d'EasyMock qui permet de mieux maintenir les tests sur le long terme en faciliant la relecture des tests implémentés. Donc rajouter une syntaxe Mockito style pourquoi pas, mais ne supprime la syntaxe initiale d'EasyMock ;o) Comme Mathilde, j'utilise PowerMock+EasyMock pour pouvoir mocker les méthodes statiques, donc ce n'est pas une fonctionnalité bloquante.
  • Hello Henri, Je suis un des committer du projet Mockito. Effectivement, tu es un pionnier, sans ton framework de mock, personne probablement n'en serait là et certainement pas nous. Grâce aux mocks générés il est possible enfin de se concentrer davantage sur les collaborations et les comportements entre les objets, bref grâce à toi on peut enfin témoigner du retour de la vrai programmation orientée objet. C'est vrai Mockito s'est effectivement bien inspiré de Easymock. D'ailleurs le code technique intrinsèque de Mockito n'utilise plus aucun code de Easymock depuis quelques temps déjà. Bref l'idée était d'augmenter l'expressivité d'un test avec le moins possible de "nuisance" de la part du framework. C'est pour cela aujourd'hui qu'un gros travail a été fait sur les annotations, ce travail continue encore aujourd'hui. L'API également a été conçue dans cet esprit afin d'exprimer vraiment l'intention du test. A cet effet également, je prône vraiment l'usage de la syntaxe BDD. C'est aussi pour ça que je ne suis pas complètement d'accord avec l'exemple de la liste : ================================================================ 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(); ================================================================ Dans la plupart des cas ce genre de stub+vérification n'a pas lieu d'être. Étant donné que la liste est vide, si le code utilise les objets retournés par les deux stubs, alors le code ne plantera pas. On peut alors se permettre de retirer les deux vérifications liés à ces appels. Ici il s'agit d'une liste, mais le cas est beaucoup plus réaliste quand on travaille avec les objets du domaine. A propos des classes final, des méthodes privées ou des méthodes statiques, je pense que ces possibilités doivent rester dans powermock, je sais que c'est parfois utile. Mais en termes de programmation / design orientée objet, devoir bouchonner ce code me propose un problème de "conscience". Encore une fois ceci dépend très fortement du contexte. Mais c'est pour ça que pour ces cas très spécifiques il y a powermock. Quant aux tests qui cassent souvent je suis d'accord la philosophie est différente, je n'ai pas encore d'avis bien déterminé à ce sujet. Pour finir, mockito rencontre également des problèmes du a des gens de l'IT qui ne jurent que par Easymock ou encore jMock ;) Voilà, chapeau bas pour tes idées et ton travail.
  • Merci pour cet article très complet et très intéressant. Et encore merci pour EasyMock. C'est toujours utile d'avoir un tel framework sous la main en cas de besoin !
  • Merci Henri pour cet article. Perso c'est surtout hamcrest qui faisait défaut à easymock jusqu'à présent. En tant qu'utilisateur, je suis plutôt d'avis qu'un peu de concurrence ne peut que booster l'innovation. En tout cas merci pour ton travail sur easymock.
  • @Brice: Merci beaucoup pour ces commentaires. Je suis d'accord pour l'exemple. Il n'est pas complètement réaliste pour soucis de rester simple. Par contre je crois que le cas où l'appel est à la fois dans le when et le verify est assez fréquent. Où devrait potentiellement l'être plus. Mais c'est discutable en effet. Powermock fait en effet très bien ce qu'il fait. L'intégration à EasyMock m'a toujours tenté car j'avais en tête de faire un peu la même chose au tout début d'EasyMock Class Extension. Mais ce n'est pas la priorité principale. Elle est plutôt sur les annotations et l'aide au mocking en général. Je fais sûrement suivre un peu vos pas là-dessus :-) Continuez à me faire avancer.
  • @Henri Pour le bout de code en question, ça dépend du code testé et du test même! Je fais du TDD avant tout pour le design et en second pour tester mon code. Du coup pour le code de production qui a besoin de données fournies par un mock alors le simple fait que le test ne plante pas à l'appel sur le mock implique une vérification implicite. Dans d'autres cas ou la vérification n'est pas implicite, il faut effectivement penser à la section de vérification. Pareil, il vaut mieux copier les bonnes idées, que de persévérer sur des mauvaises! ;)
  • Merci beaucoup pour cet article si clair et si riche. Je suis entrain de faire des tests unitaires avec Junit et je m'ensert de easymock pour tous ce qui mock. Mon probleme, que j'arrive pas à contourné, c'est que je dois tester une méthode qui instancie une classe mockés. Quelqu'un pourra t il m'eclairé plus sur comment tester un tel cas? Merci d'avance.
  • @aurban: Je ne suis pas sûr de comprendre. La classe a tester instancie une classe qui doit être mockée? Si c'est le cas, c'est impossible et le code doit être refactoré pour devenir testable. La classe à mocker doit être injectée. Si je n'ai rien compris, je veux bien un exemple :-)
  • Bonjour, est-ce possible de faire fonctionner EasyMockSupport.replayAll() et verifyAll() avec des mocks créés par Spring et injectés dans la classe de test ? Merci !
  • @dju Il est recommandé d'utiliser la mailing list d'EasyMock (http://groups.yahoo.com/group/easymock) pour ce genre de question. Néanmoins, si EasyMockSupport est utilisé comme factory spring pour créer les mocks, il sera possible de faire le replayAll/verifyAll sur ces mocks.
    1. Laisser un commentaire

      Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *


      Ce formulaire est protégé par Google Recaptcha