java.lang.IllegalStateException: 1 matchers expected, 2 recorded
« java.lang.IllegalStateException: 1 matchers expected, 2 recorded » est une exception bien connue du développeur utilisant EasyMock. Elle veut tout simplement dire que lorsque l’on utilise un matcher (genre eq,anyObject …) il faut en utiliser pour tous les paramètres de la méthode, j’y reviendrai plus en détail dans un prochain article. Ainsi :
expect(maMethode("XYZ","BZT")).andReturn("XXX") // est correct tout comme : expect(maMethode((String)anyObject(),eq("BZT"))).andReturn("XXX") // mais pas : expect(maMethode((String)anyObject(),"BZT")).andReturn("XXX")
Sauf que mon erreur du jour, c’est que cette expression arrive sur la méthode :
« expect(formatServiceMock.scale(eq(BigDecimal.valueOf(10)))).andReturn(c) » qui elle est correcte a première vue.
Le test passe unitairement dans Eclipse et dans Surefire. L’ensemble des tests passent dans Eclipse, il n’y a que dans lors de la tâche « install » de Maven que l’erreur se produit (et non, Maven n’a rien à voir là dedans :p).
Si on zoome sur le code simplifié au maximum, on obtient :
Une classe et son interface, FormatServiceImpl et FormatService qui ne contiennent que 2 méthodes vides
public class FormatServiceImpl implements FormatService { private static final int SCALE = 5; public void scale(BigDecimal a) { } public void scale2(BigDecimal a) { }
Une classe et son interface, « CalculServiceImpl » et « CalculService » qui contient une méthode et un champ privé de type « FormatService ».
public class FormatServiceImpl implements public class CalculServiceImpl implements CalculService { private final FormatService formatService; public CalculServiceImpl( FormatService formatService) { this.formatService = formatService; } public BigDecimal addAndscale(BigDecimal a, BigDecimal b) { formatService.scale(a.add(b)); return a.add(b); } }
La classe de test de « CalculServiceImpl » contient une méthode de test « testAddAndScale » qui pose problème uniquement dans le cas d’une exécution des tests via Surefire, le plugin Maven.
public class CalculServiceImplTest { private CalculServiceImpl calculService; private FormatService formatServiceMock; @Before public void before() { formatServiceMock = createMock(FormatService.class); calculService = new CalculServiceImpl( formatServiceMock); } @Test public void testAdd() { BigDecimal a = BigDecimal.valueOf(5); BigDecimal b = BigDecimal.valueOf(5); BigDecimal c = BigDecimal.valueOf(10); formatServiceMock.scale(eq(BigDecimal .valueOf(10))); replay(formatServiceMock); assertEquals(c, calculService.addAndscale(a, b)); verify(formatServiceMock); }
Le fonctionnement de cette classe est classique : on définit un mock, ici : « formatServiceMock » que l’on affecte à l’implémentation testée calculService. On lui affecte un comportement spécifique : la méthode « scale » de « FormatService » est appelée avec le paramètre équivalent à « BigDecimal.valueOf(10) ». Le mock est ensuite chargée avec la méthode « replay(formatServiceMock) ».
L’action testée est ensuite lancée : « calculService.addAndscale(a,b) » que l’on vérifie être égale à « c ». Puis avec : « verify(formatServiceMock) », on vérifie que le « mock formatServiceMock » a bien eu le comportement attendu c’est à dire que la méthode « scale » de « FormatService » a bien été appelée avec le paramètre équivalent à BigDecimal.valueOf(10) .
Cette classe de test ne soulève une erreur que lorsqu’elle est appelé via les test Surefire de Maven. Si on la lance unitairement via Eclipse (Run as Junit test) ou via Maven sSurefire individuellement (via mvn -Dtest=CalculServiceImplTest test) aucun problème. Si on lance l’ensemble des tests unitaires via Eclipse (Run as JUnit test) idem aucun problème.
Par contre, si on la lance avec l’ensemble des tests Surefire, on obtient l’erreur :
java.lang.IllegalStateException: 1 matchers expected, 2 recorded. at org.easymock.internal.ExpectedInvocation.createMissingMatchers(ExpectedInvocation.java:56) at org.easymock.internal.ExpectedInvocation.<init>(ExpectedInvocation.java:48) at org.easymock.internal.ExpectedInvocation.<init>(ExpectedInvocation.java:40) at org.easymock.internal.RecordState.invoke(RecordState.java:76) at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:38) at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:72) at $Proxy5.scale(Unknown Source) at fr.java.freelance.easymock.CalculServiceTest.testAdd(CalculServiceTest.java:40)
Impossible dans un premier temps de voir d’où vient l’erreur. On utilise alors le système permettant de débugger les tests à distance à l’aide de : « mvn -Dmaven.surefire.debug test ». Lorsque l’on lance la commande, l’exécution se met en pause tant qu’elle n’a pas reçu de connexion sur son port 5005 (en configuration par défaut, customizable).
[INFO] Scanning for projects... [INFO] ------------------------------------------------------------------------ [INFO] Building EasyMockTest [INFO] [INFO] Id: fr.java.freelance:TestSpringPath:jar:0.0.1-SNAPSHOT [INFO] task-segment: [test] [INFO] ------------------------------------------------------------------------ [INFO] [resources:resources] [INFO] Using default encoding to copy filtered resources. [INFO] [compiler:compile] [INFO] Nothing to compile - all classes are up to date [INFO] [resources:testResources] [INFO] Using default encoding to copy filtered resources. [INFO] [compiler:testCompile] [INFO] Nothing to compile - all classes are up to date [INFO] [surefire:test] [INFO] Surefire report directory: E:\workspace\EasyMock\target\surefire-reports Listening for transport dt_socket at address: 5005
Il suffit alors dans Eclipse d’aller dans le mode Debug Configurations et de créer une remote application sur le port 5005 et on peut débugger le test dans Eclipse .
Un moyen de voir le nombre d’objet enregistré dans EasyMock est de mettre un point d’arrêt juste avant la levée de l’exception : la méthode mockée doit avoir autant d’arguments que la liste enregistrée. Ici, la méthode est appelée avec un argument (on ne se formalise pas sur le null, seul compte ici le nombre d’argument) alors que la liste en possède 2 : un matcher « Any » et un matcher « Equals », d’où le « IllegalStateException ».
Cliquer pour voir en grand :
La recherche pour savoir d’où provient le « Any » est assez fastidieuse (le « Equals » étant celui du test). Les objets enregistrés le sont via un autre chemin et partagés via l’objet « LastControl ». Pour comprendre comment les matchers sont enregistrés, il suffit de faire un pas à pas en mode debug.
Si on entre (via F5) dans le détail de l’appel à « formatServiceMock.scale(eq(BigDecimal
.valueOf(10))); », on arrive tout d’abord sur le calcul du « valueOf » puis sur la méthode « eq » de la classe EasyMock :
public static <T> T eq(T value) { reportMatcher(new Equals(value)); return null; }
On entre à nouveau dans « reportMatcher(new Equals(value)); »
En continuant un peu avec F5 on arrive à dans la classe « EasyMock » :
/** * Reports an argument matcher. This method is needed to define own argument * matchers. For details, see the EasyMock documentation. * * @param matcher */ public static void reportMatcher(IArgumentMatcher matcher) { LastControl.reportMatcher(matcher); }
On continue à regarder ce qui se passe dans « LastControl ». On arrive sur une classe contenant plusieurs threads : « threadToControl » – « threadToCurrentInvocation » – « threadToArgumentMatcherStack ». Celui qui nous intéresse est « threadToArgumentMatcherStack ». Il contient une pile d’ « ArgumentMatcher ».Le « reportMatcher » permet d’ajouter chacun des matchers pour chacun des arguments. A cet étape là du calcul, cette pile doit être vide.
public static void reportMatcher(IArgumentMatcher matcher) { Stack stack = threadToArgumentMatcherStack.get(); if (stack == null) { stack = new Stack(); threadToArgumentMatcherStack.set(stack); } stack.push(matcher); }
Comme attendu, la pile n’est pas vide, une autre méthode y a déjà renseigné une valeur …
En supprimant des tests, je suis tombée sur la méthode fautive en utilisant un point d’arrêt sur ce chemin , tout au fond d’un des tests d’intégration, dans une partie où EasyMock n’était pas du tout utilisée et sur des classes n’ayant aucun rapport avec les classes testées, il y avait quelque chose comme cela:
@Test public void testJunitMalConstruit() { FormatServiceImpl formatService = new FormatServiceImpl(); formatService.scale2((BigDecimal) anyObject()); }
Le « (BigDecimal) anyObject() » est ici une erreur d’étourderie qui au final se retrouve à avoir un impact très loin dans le code. C’est pour moi un bug d’EasyMock, même si ce « (BigDecimal) anyObject() » n’a rien à faire ici !
Le fait que les tests passent ou non dans Surefire ou dans Eclipse via « run as JUnit test » dépend en fait de l’ordre dans lequel les 2 lanceurs de tests ordonnent les différents tests. Pour enlever cette notion variable, on peut utiliser le test suivant :
public class CalculServiceImplTest { private CalculServiceImpl calculService; private FormatService formatServiceMock; @Before public void before() { formatServiceMock = createMock(FormatService.class); calculService = new CalculServiceImpl( formatServiceMock); } @Test public void testJunitMalConstruit() { FormatServiceImpl formatService = new FormatServiceImpl(); formatService.scale2((BigDecimal) anyObject()); } @Test public void testAdd() { BigDecimal a = BigDecimal.valueOf(5); BigDecimal b = BigDecimal.valueOf(5); BigDecimal c = BigDecimal.valueOf(10); formatServiceMock.scale(eq(BigDecimal .valueOf(10))); replay(formatServiceMock); calculService.addAndscale(a, b); verify(formatServiceMock); } }
Et là ca plante à chaque fois
Et pour faire plaisir aux adorateurs de Mockito, j’ai testé avec Mockito la classe ci-dessous :
public class CalculServiceImplMockitoTest { private CalculServiceImpl calculService; private FormatService formatServiceMock; @Before public void before() { formatServiceMock = mock(FormatService.class); calculService = new CalculServiceImpl( formatServiceMock); } @Test public void testJunitMalConstruit() { FormatServiceImpl formatService = new FormatServiceImpl(); formatService.scale2((BigDecimal) anyObject()); } @Test public void testAdd() { BigDecimal a = BigDecimal.valueOf(5); BigDecimal b = BigDecimal.valueOf(5); BigDecimal c = BigDecimal.valueOf(10); formatServiceMock.scale(eq(BigDecimal .valueOf(10))); calculService.addAndscale(a, b); verify(formatServiceMock); } }
Et l’erreur est ici bien claire :
org.mockito.exceptions.misusing.InvalidUseOfMatchersException: Misplaced argument matcher detected here: -> at fr.java.freelance.easymock.CalculServiceImplMockitoTest.testJunitMalConstruit(CalculServiceImplMockitoTest.java:24) You cannot use argument matchers outside of verification or stubbing. Examples of correct usage of argument matchers: when(mock.get(anyInt())).thenReturn(null); doThrow(new RuntimeException()).when(mock).someVoidMethod(anyObject()); verify(mock).someMethod(contains("foo"))
Mockito 1 – EasyMock 0
Je trépignais du début de l’article pour répéter à quel point Mockito est agréable à utiliser sans pour autant paraitre trop lourd… Même pas eu à le faire !
@David: même si je suis un fervent admirateur de Mockito, je n’irai pas non plus jusqu’à dire qu’il est tout le temps le sauveur. Ce genre de cas où les tests passent dans Eclipse individuellement et pas quand ils sont lancés tous, j’en ai déjà rencontré aussi avec Mockito (notamment lors de l’utilisation de spy et de compteurs sur le nombre d’executions d’une méthode)[ même si depuis que j'ai suivi ta prestation sur Mockito au chtijug j'ai réalisé que compter le nombre d'éxecution d'une méthode n'était pas forcément le meilleur moyen de valider un comportement attendu...]
@Mathilde: respect pour ta ténacité dans la recherche du fautif (et pour la retranscription sous forme de post)
Pour avoir testé EasyMock et Mockito sur deux projets différents, j’ai pu constater que Mockito renvoyait effectivement des messages d’erreurs bien plus parlant. C’est dingue comme un simple message du style « you should use eq(…) for native type » dans l’exception peut vous faire gagner comme temps, par exemple.