Simulacres de tests avec EasyMock et JUnit 4

Baptiste Wicht

Cet article va vous présenter l'utilisation de Easymock et de JUnit 4 pour effectuer des tests unitaires avec des simulacres de tests.

- Titre : Simulacres de tests avec EasyMock et JUnit 4

- Auteur : Baptiste Wicht

- Parution : 7 mars 2010

- Mise à jour : 2 juin 2020

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2009 Baptiste Wicht. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Vérifier un comportement

- III. Attendre des valeurs de retour

- IV. Traiter les exceptions

- V. Divers

- V-A. Vérifier le nombre d'appels

- V-B. Vérifier l'ordre des appels

- V-C. Mocker une classe

- VI. Conclusion

I - Introduction

Les objets simulacres permettent d'effectuer des tests unitaires sur un objet dépendant d'autres objets. On va remplacer ces objets dont dépend l'objet à tester par des simulacres. On va, par exemple pouvoir vérifier que la méthode xyzzy() a été appelée 5 fois et a retourné 33. Cela peut être pratique dans bien des cas. Par exemple si l'objet réel (celui qu'on mock) est lent ou non déterministe (dépendant du temps ou même de la météo). Ces objets sont très difficiles à tester, car on pourrait faire plein de tests sans jamais tomber sur les cas spéciaux. Les cas de tests nous permettront de traiter ces cas spéciaux.

Il existe plusieurs outils permettant de faire des objets simulacres. Dans cet article, nous allons utiliser EasyMock 2.5.2 pour effectuer ces tests. Pour ce qui est des tests, nous allons utiliser JUnit 4.7.

Voici l'interface à tester :

public interface ISimpleDao {
void save(String title);
void remove(String title) throws NotExistingException;
int count();
void debug();
boolean isValid(String title);
void insert(String title);
}

Et voici notre classe à tester :

public class SimpleService {
private ISimpleDao dao;

public void setDao(ISimpleDao dao){
this.dao = dao;
}

public void insert(String title){
if(dao.isValid(title)){
dao.insert(title);
}
}

public void save(String... titles){
for(String title : titles){
dao.save(title);
}
}

public boolean remove(String title){
try {
dao.remove(title);
} catch (NotExistingException e){
return false;
}

return true;
}

public int size(){
return dao.count();
}

public void debug(){
System.out.println("Debug information of SimpleService");
dao.debug();
}
}

Notre mock va donc implémenter l'interface ISimpleDao et nous allons le passer à SimpleService qui est la classe à tester dans notre cas. Cet exemple est vraiment simpliste. Dans les cas pratiques, vous ferez face à des cas beaucoup plus complexes, mais cet exemple permettra de couvrir la plupart des fonctionnalités d'EasyMock.

La vérification se fait essentiellement via deux méthodes :

- expect(T value) : Permet de spécifier une valeur de retour attendue ;

- expectLastCall() : à utiliser pour les méthodes ne retournant rien.

Ces deux méthodes renvoient un objet IExpectationSetters qui permet de configurer ce que l'on attend de la méthode mockée par exemple le nombre de fois que la méthode doit être appelée.

II - Vérifier un comportement

Voici la structure de base pour notre classe de test :

import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.*;
import static org.easymock.EasyMock.*;

public class SimpleServiceTest {
private SimpleService simpleService;
private ISimpleDao simpleDaoMock;

@Before
public void setUp(){
simpleService = new SimpleService();
simpleService.setDao(simpleDaoMock);
}

@Test
public void insertValid(){}

@Test
public void insertNotValid(){}

@Test
public void save(){}

@Test
public void removeWithoutException() throws NotExistingException {}

@Test
public void removeWithException() throws NotExistingException {}

@Test
public void size(){}

@Test
public void debug(){}
}

Premièrement, nous allons commencer par créer un objet simulacre (un mock). Pour cela, il nous faut utiliser la classe EasyMock et sa méthode createMock() qui prend en paramètre l'interface que doit implémenter le mock. Pour améliorer la clarté du code, on va utiliser un import statique comme pour JUnit.

import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.*;
import static org.easymock.EasyMock.*;

public class SimpleServiceTest {
private SimpleService simpleService;
private ISimpleDao simpleDaoMock;

@Before
public void setUp(){
simpleDaoMock = createMock(ISimpleDao.class);

simpleService = new SimpleService();
simpleService.setDao(simpleDaoMock);
}
}

Cela va simplement créer un objet mock implémentant l'interface ISimpleDao. La première action que nous pouvons entreprendre avec EasyMock est de vérifier qu'une méthode a bien été appelée. Avec EasyMock, cela fonctionne comme un enregistrement :

- on joue la séquence désirée sur l'objet mock ;

- on enregistre la séquence jouée ;

- on teste l'objet ;

- on vérifie si la séquence a été correctement rejouée.

On va donc simplement tester pour commencer si la méthode debug() de SimpleService appelle bien la méthode debug() de notre classe DAO (Data Access Object) :

@Test
public void debug(){
simpleDaoMock.debug();

replay(simpleDaoMock);

simpleService.debug();

verify(simpleDaoMock);
}

La méthode replay() permet de sauvegarder l'enregistrement et la méthode verify() permet de vérifier que ce qui est fait après replay() est bien conforme à l'enregistrement. Si vous lancez le test, il va être validé. Maintenant, si on commente dao.debug() dans SimpleService, le test ne va pas se dérouler correctement :

java.lang.AssertionError:
Expectation failure on verify:
debug(): expected: 1, actual: 0
at org.easymock.internal.MocksControl.verify(MocksControl.java:111)
at org.easymock.EasyMock.verify(EasyMock.java:1608)
at com.dvp.wichtounet.easymock.SimpleServiceTest.debug(SimpleServiceTest.java:45)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:94)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:165)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:60)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:110)

EasyMock détecte donc bien que notre méthode n'a pas été appelée au contraire de ce qui a été spécifié par l'enregistrement et, de ce fait, le test échoue. Si vous essayez de vérifier si des méthodes non-void ont bien été appelées de la même manière, vous devriez avoir une exception de type IllegalStateException. En effet, pour les méthodes retournant quelque chose, il faut indiquer à EasyMock ce qu'il faut retourner. Nous allons voir cela au prochain chapitre.

III - Attendre des valeurs de retour

Nous allons maintenant traiter des méthodes qui retournent quelque chose. Dans ce cas, il faut définir un comportement pour pouvoir vérifier ce comportement. Pour ce faire, il faut utiliser la méthode expect() et andReturn() pour spécifier une valeur de retour. Voici comment cela pourrait s'écrire pour le test de la méthode size() :

@Test
public void size(){
expect(simpleDaoMock.count()).andReturn(32);

replay(simpleDaoMock);

assertEquals(32, simpleService.size());

verify(simpleDaoMock);
}

Par ces quelques lignes de code, on fait 2 tests. On vérifie si la méthode count() a bien été appelée et si size() retourne la même valeur que count(). Ce qui est bien le cas si on lance le test. On vientdonc voir qu'il est très simple de spécifier une valeur de retour pour une méthode sur un objet mock.

IV - Traiter les exceptions

EasyMock permet également de traiter les exceptions. Il nous faudra à nouveau utiliser la méthode expect(), mais, cette fois, au lieu de spécifier une valeur de retour, on va spécifier une exception qu'il faut lever avec la méthode andThrow(). Voyons ce que ça donnerait avec le test de la méthode remove() avec et sans exception :

//Sans exception
@Test
public void removeWithoutException() throws NotExistingException {
simpleDaoMock.remove("Mary");

replay(simpleDaoMock);

assertTrue(simpleService.remove("Mary"));

verify(simpleDaoMock);
}

//Avec exception
@Test
public void removeWithException() throws NotExistingException {
simpleDaoMock.remove("Arthur");

expectLastCall().andThrow(new NotExistingException());

replay(simpleDaoMock);

assertFalse(simpleService.remove("Arthur"));

verify(simpleDaoMock);
}

Encore une fois, il nous a suffi d'un seul appel de méthode pour spécifier une exception et, ensuite, nous avons pu vérifier le comportement de notre service en fonction du comportement de notre mock.

V - Divers

V-A - Vérifier le nombre d'appels

Testons maintenant notre méthode save() :

@Test
public void save(){
simpleDaoMock.save("xyzzy");
simpleDaoMock.save("xyzzy");
simpleDaoMock.save("xyzzy");
simpleDaoMock.save("xyzzy");
simpleDaoMock.save("xyzzy");

replay(simpleDaoMock);

simpleService.save("xyzzy", "xyzzy", "xyzzy", "xyzzy", "xyzzy");

verify(simpleDaoMock);
}

Ce genre de code devient vite très lourd à écrire en fonction du nombre d'appels. On a 2 solutions : soit on fait une boucle pour appeler les méthodes du mock, soit on utilise les fonctionnalités d'EasyMock qui permet de spécifier le nombre de fois qu'une méthode doit être appelée grâce à la méthode times() :

@Test
public void save(){
simpleDaoMock.save("xyzzy");

expectLastCall().times(5);

replay(simpleDaoMock);

simpleService.save("xyzzy", "xyzzy", "xyzzy", "xyzzy", "xyzzy");

verify(simpleDaoMock);
}

Beaucoup plus clair, non ? Il est également possible de spécifier qu'une méthode peut être appelée un nombre indéfini de fois avec la méthode anyTimes() ou encore un certain nombre de fois compris dans un intervalle avec la méthode times(min, max).

V-B - Vérifier l'ordre des appels

On va maintenant tester notre méthode insert() :

@Test
public void insertValid(){
expect(simpleDaoMock.isValid("Arthur")).andReturn(true);

simpleDaoMock.insert("Arthur");

replay(simpleDaoMock);

simpleService.insert("Arthur");

verify(simpleDaoMock);
}

@Test
public void insertNotValid(){
expect(simpleDaoMock.isValid("Arthur")).andReturn(false);

replay(simpleDaoMock);

simpleService.insert("Arthur");

verify(simpleDaoMock);
}

Vous allez me dire qu'on a déjà vu tout cela, certes, mais dans ce genre de cas, il peut également être intéressant de vérifier que les appels se font au bon endroit. En effet, si la méthode isValid() est appelée après que l'insert() ait été fait, elle n'a pas beaucoup d'intérêt. Avec EasyMock, il y a deux façons de vérifier l'ordre des appels. Soit on utilise la méthode createStrictMock() au lieu de createMock() ou alors on utilise checkOrder(mock, true) pour activer la vérification de l'ordre. Un mock strict est un bouchon qui va vérifier l'ordre des appels. On n'a donc pas besoin de changer nos tests, il suffit d'utiliser une de ces deux méthodes dans notre méthode before().

V-C - Mocker une classe

EasyMock met à disposition une extension pour créer un mock d'une classe et non d'une interface. Il s'agit d'EasyMock Class Extension. L'utilisation de base reste la même, il faut juste changer l'import :

import static org.easymock.classextension.EasyMock.*;

En plus de cela, il est également possible d'effectuer un mocking partiel en ne mockant par exemple qu'une seule méthode :

Mocked mock = createMockBuilder(Mocked.class).addMockedMethod("mockedMethod").createMock();

Il faut toutefois faire attention au fait que les classes finales ne sont pas supportées. Si la classe contient des méthodes finales, elles ne seront pas mockées et elles seront appelées normalement.

VI - Conclusion

Voilà, nous avons maintenant passé en revue les principales fonctionnalités que nous offre EasyMock pour la création d'objets mocks pour les tests unitaires. Comme vous avez pu le constater, c'est un moyen simple, mais très puissant de vérifier le comportement d'un objet en fonction du comportement d'un objet dont il dépend. Il existe d'autres librairies qu'EasyMock pour faire cela comme JMock, JMockit ou encore Mockito. Personnellement, je trouve qu'EasyMock est la plus agréable à utiliser et fournit toutes les fonctions dont j'ai besoin pour faire mes tests, c'est pourquoi j'ai choisi de présenter cette librairie.

Vous pouvez télécharger les sources de cet article ici.

Si vous êtes intéressé par un autre framework, je vous invite à lire cet article sur JMockit, de Florence Chabanois.

Pour vos questions sur les outils de test, vous pouvez consulter le forum Developpez.com sur les outils de tests.

Pour plus d'informations sur EasyMock, je vous invite à consulter la documentation officielle en français.

Un grand merci à karl3i pour ses corrections orthographiques.