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.