I. Qu'est-ce que JMockit ?▲
JMockit est un framework de mocking pour les tests unitaires : il permet de créer des simulacres.
Encore un ? Il y en a déjà et ils font bien leur travail : JMock, Easymock et Mockito par exemple. C'est exact, ces frameworks sont très bien… quand le code est testable. Les applications sur lesquelles nous travaillons sont malheureusement envahies de composants intestables. Cela constitue autant de failles si nous nous résignons à ne pas les tester. Comme le montre Misko Hevery dans son guide, un code testable comporte les caractéristiques suivantes :
- le constructeur ne fait aucun travail : il se contente d'assigner les paramètres aux attributs de la classe ;
- il n'y a pas d'état global : pas de singleton, de variable statique, de méthode statique ;
- les méthodes ne « creusent » pas les collaborateurs pour obtenir d'autres objets. Les collaborateurs sont directement utilisés, ils ne servent pas d'intermédiaire pour atteindre l'objet dont on a réellement besoin ;
- une classe a peu, voire une seule responsabilité
Les deux derniers critères complexifient l'écriture de tests, mais ne la rendent pas impossible. Le fait qu'il n'y ait pas de référence sur les collaborateurs du fait d'instanciation explicite d'objets ou qu'il y ait des états globaux empêche réellement toute tentative de test : nous ne pouvons simplement pas injecter de mock.
public
class
MyClass
{
public
MyClass
(
)
{
MyService service =
new
HttpService
(
);
// whatever
}
}
Nous ne voulons certainement pas nous traîner un véritable HttpService dans nos tests, avec ce que sa construction implique (un client HTTP qui tourne par exemple). Pour pouvoir injecter un faux MyService, nous transformons la variable service en attribut de classe et ajoutons un paramètre dans le constructeur.
public
class
MyClass
{
private
MyService service;
public
MyClass
(
MyService service)
{
this
.service =
service;
//whatever
}
}
MockService mockService =
new
MockService
(
);
MyClass myClass =
new
MyClass
(
mockService);
Quand nous rencontrons ce genre de code, il faut donc respirer un bon coup et refactorer. Mais souvent, le client préfère que l'on travaille sur de nouvelles features plutôt que du refactoring sans valeur visuelle, le temps presse, l'équipe est déjà en retard, cette fonctionnalité n'existera plus dans un an, une refonte technique est déjà prévue, personne ne sait ce que ce code fait, mais il marche alors pas touche, le code spaghetti me donne des nausées… Autant de raisons font que le refactoring n'est pas toujours possible.
C'est ici que JMockit intervient : il permet de définir des mocks pour des classes, même si nous n'avons pas de référence dessus. Il ne s'agit plus de créer une instance de mock mais de mocker toutes les instances d'une classe. Ainsi, les états globaux ne sont plus un problème. Le framework permet également de mocker des constructeurs, des méthodes statiques, des blocs statiques. Les instanciations directes ne nous gênent plus non plus. JMockit peut même mocker des méthodes privées !
Le framework est constitué de plusieurs API : JMockit Core, JMockit Annotations, JMockit Expectations et depuis peu JMockit Verifications. Ce tutoriel n'évoquera pas tous les aspects du framework, loin de là, mais suffisamment pour vous donner une idée de sa puissance et l'envie d'aller plus loin.
Les exemples ont été testés avec JMockit 0.993 sous une JDK6.
II. Installation de JMockit▲
Première étape : télécharger le JAR sur le site de JMockit : http://code.google.com/p/jmockit/downloads/list.
Parce qu'il s'appuie sur l'instrumentation, JMockit ne fonctionne que depuis Java 5.
II-A. Classiquement▲
Le jar de JMockit contenant le nécessaire, il suffit de l'ajouter au classpath.
II-B. Avec Maven▲
JMockit est hébergé sur java.net : http://download.java.net/maven/2/mockit/jmockit/.
Pour l'utiliser, ajouter l'artefact dans le pom.xml :
<dependency>
<groupId>
mockit</groupId>
<artifactId>
jmockit</artifactId>
<version>
0.993</version>
</dependency>
III. Lancer les tests▲
III-A. Sous Java 5▲
Pour lancer les tests avec JMockit sous Java 5, la JVM a besoin de savoir où est le JAR via l'option javaagent.
III-A-1. Avec Eclipse▲
Au moment de lancer les tests, dans le runner, il faut ajouter l'argument à la JVM :
-javaagent:C:\.m2\repository\mockit\jmockit\0.993\jmockit-0.993.jar
Le recours à une variable de substitution facilite les montées de version :
-javaagent:${JMOCKIT_JAR}
III-A-2. Avec Maven▲
<plugin>
<artifactId>
maven-surefire-plugin</artifactId>
<configuration>
<argLine>
-javaagent:"${settings.localRepository}/mockit/jmockit/0.993/jmockit-0.993.jar"</argLine>
<useSystemClassLoader>
true</useSystemClassLoader>
</configuration>
</plugin>
III-B. Sous Java 6▲
Il n'y a rien à faire : ni d'annotation @RunWith à mettre sur la classe, ni de javaagent à spécifier.
Si vous développez sous Mac OS X, ajoutez <jdkDir>/lib/tools.jar à votre classpath, où <jdkDir> est la racine de votre JDK 6 locale.
Si JMockit ne fonctionne pas sous votre environnement en dépit de votre JDK 6, ajoutez le paramètre javaagent comme pour une JDK 5. Cela arrive notamment avec la version IBM J9 JDK 1.6.
Si vous rencontrez d'autres problèmes, la documentation d'install officielle peut vous aider.
Pour information, les anciennes versions de JMockit nécessitaient de restaurer manuellement les définitions originales. Autrement, les mocks définis dans les tests précédents perturbaient le test courant.
@After
public
void
tearDown
(
) {
// to avoid pertubation on other junit test
Mockit.restoreAllOriginalDefinitions
(
);
}
Cette précaution n'est plus nécessaire.
IV. JMockit Core et JMockit Annotations▲
JMockit Core permet notamment de substituer tous les appels à une classe A par une classe B, B étant une classe de mock. Cette API a le mérite d'être simple à comprendre, mais rend le test un peu moins lisible dans la mesure où il faut déclarer une classe de mock.
JMockit Annotations présente les mêmes possibilités que Core, avec en plus des vérifications sur le nombre de fois où une méthode est invoquée. Comme les appels sont vérifiés, il est possible d'aussi vérifier les paramètres passés. L'API a cependant le même problème de lisibilité que JMockit Core, pour la même raison.
Dans les deux cas, si une méthode de la classe A est appelée et que B ne la définit pas, alors c'est la méthode réelle qui est appelée. Aussi, il est plus juste de parler de redéfinition de méthodes que de redéfinition de classe, que l'on peut voir dans les autres frameworks de mocks.
IV-A. Redéfinir une méthode▲
L'idée est de dire qu'à chaque fois qu'une classe sera sollicitée, ce sera notre mock qui sera utilisé à sa place. Cela est pratique quand la classe que l'on souhaite mocker n'est pas accessible. Nous souhaitons tester la méthode doOperation :
package
fr.fchabanois.tutorial.jmockit;
public
class
MyService
{
public
void
doOperation
(
String value)
{
DatabaseLogger logger =
new
DatabaseLogger
(
);
logger.info
(
"starting operation : "
+
value);
// Execution de l'opération
// ....
// ....
// ....
}
}
Nous ne souhaitons pas que notre test écrive quoi que ce soit en base à chaque fois qu'il est lancé. Or le databaseLogger n'est pas une instance passée en paramètre, ce qui fait que nous ne pouvons pas le mocker tel que le code est écrit. L'idéal serait de refactorer le code, mais nous ne pouvons parfois pas nous le permettre.
IV-A-1. Avec JMockit Core▲
JMockit Core permet de résoudre ce genre de problématique avec la méthode redefineMethods.
package
fr.fchabanois.tutorial.jmockit;
import
mockit.Mockit;
import
org.junit.Test;
public
class
MyServiceTest
{
@Test
public
void
testDoOperation_nominalCase
(
)
{
MyService service =
new
MyService
(
);
// Definition du mock :
// la classe MockDatabaseLogger sera utilisée à la place de DatabaseLogger
// à chaque fois que cette dernière sera appelée
Mockit.redefineMethods
(
DatabaseLogger.class
, MockDatabaseLogger.class
);
service.doOperation
(
"anything"
);
}
public
static
class
MockDatabaseLogger
{
public
Object info
(
String sentence) {
System.out.println
(
"MOCK DatabaseLogger.info()"
);
return
null
;
}
}
}
En lançant le test, nous constatons que c'est bien la méthode info de MockDatabaseLogger qui est appelée grâce à la console :
MOCK DatabaseLogger.info()
Techniquement, vous pouvez valider les paramètres passés dans le mock et retourner de faux objets. Notez que ce n'est pas forcément pertinent, car JMockit Core ne vérifie pas que la méthode est appelée. Si vous avez besoin de vérifier l'appel, orientez-vous plutôt vers JMockit Annotations ou JMockit Expectations. Voici un exemple de vérification de paramètre :
public
static
class
MockDatabaseLogger
{
public
Object info
(
String sentence) {
System.out.println
(
"MOCK DatabaseLogger.info()"
);
assertThat
(
sentence, is
(
"starting operation : "
+
sentence));
return
null
;
}
}
et un autre encore plus précis :
package
fr.fchabanois.tutorial.jmockit;
import
static
org.hamcrest.CoreMatchers.is;
import
static
org.junit.Assert.assertThat;
import
mockit.Mockit;
import
org.junit.Test;
public
class
MyServiceTest
{
@Test
public
void
testDoOperation_nominalCase
(
)
{
MyService service =
new
MyService
(
);
// Definition du mock
Mockit.redefineMethods
(
DatabaseLogger.class
, MockDatabaseLogger.class
);
final
String param =
"anything"
;
MockDatabaseLogger.expectedParam =
"starting operation : "
+
param;
service.doOperation
(
param);
}
public
static
class
MockDatabaseLogger
{
public
static
String expectedParam;
public
Object info
(
String sentence) {
System.out.println
(
"MOCK DatabaseLogger.info()"
);
assertThat
(
sentence, is
(
expectedParam));
return
null
;
}
}
}
Maintenant, que se passe-t-il si une méthode de DatabaseLogger est appelée, mais que MockDatabaseLogger ne la définit pas ? Testons.
DatabaseLogger comporte aussi une méthode intitulée trace() :
public
void
trace
(
String sentence) {
System.out.println
(
"DatabaseLogger.trace - "
+
sentence);
}
Modifions la classe de service pour invoquer trace.
public
class
MyService
{
public
void
doOperation
(
String value)
{
DatabaseLogger logger =
new
DatabaseLogger
(
);
logger.info
(
"starting operation : "
+
value);
// Execution de l'opération
// ....
// ....
// ....
logger.trace
(
"coucou"
);
}
}
MOCK DatabaseLogger.info
(
)
DatabaseLogger.trace -
coucou
En conclusion, si le mock ne redéfinit pas la méthode, alors c'est la méthode de l'objet original qui est appelée.
IV-A-2. Avec JMockit Annotations▲
Nous venons de voir comment redéfinir une classe (DatabaseLogger) qu'une autre classe (MyService) utilisait.
Or, le fait de pouvoir redéfinir des méthodes nous permet d'aller plus loin : de tester l'invocation d'une méthode de la classe que nous sommes en train de tester. Il s'agit en quelque sorte d'un mock partiel.
Testons la méthode doSport() de HumanBeing. Nous voulons que cette dernière appelle la méthode dress().
public
class
HumanBeing extends
AbstractLivingBeing {
protected
void
doSport
(
){
}
protected
void
dress
(
){
}
}
Autrement dit, nous avons besoin de solliciter la véritable méthode doSport(), mais de mocker dress(). Une tactique pour contourner ce problème est d'étendre la classe testée et de surcharger la méthode que l'on souhaite mocker.
public
static
class
MockHumanBeingDress extends
HumanBeing {
static
boolean
dressHasBeenCalled =
false
;
protected
void
dress
(
){
dressHasBeenCalled =
true
;
}
}
@Test
public
void
testDoSport_mockMaison
(
){
HumanBeing human =
new
MockHumanBeingDress
(
);
human.doSport
(
);
Assert.assertTrue
(
"dress has not been called!"
,MockHumanBeingDress.dressHasBeenCalled);
}
JMockit permet de le faire de façon plus élégante avec les annotations, sans avoir besoin de déclarer une variable temporaire pour flaguer l'appel de la méthode dress :
@Test
public
void
testDoSport
(
){
HumanBeing human =
new
HumanBeing
(
);
Mockit.setUpMocks
(
MockHumanBeing.class
);
human.doSport
(
);
}
@MockClass
(
realClass =
HumanBeing.class
)
public
static
class
MockHumanBeing {
@Mock
(
invocations =
1
)
protected
void
dress
(
){
}
}
Mockit.setUpMocks (attention au s à la fin !) permet de déclarer le mock.
@MockClass précise quelle classe le mock remplacera.
@Mock(invocations = 1) vérifie que la méthode annotée n'est appelée qu'une seule fois.
Admettons qu'en fonction du type de sport, nous souhaitions passer un certain type de vêtement :
protected
void
doSport
(
String typeOfSport){
}
protected
void
dress
(
String typeOfClothes){
}
Le test deviendrait :
@Test
public
void
testDoSportWithString
(
){
HumanBeing human =
new
HumanBeing
(
);
Mockit.setUpMocks
(
MockHumanBeing_dressWithStringCalledOnce.class
);
MockHumanBeing_dressWithStringCalledOnce.expectedCloth =
"short"
;
human.doSport
(
"basket"
);
}
@MockClass
(
realClass =
HumanBeing.class
)
public
static
class
MockHumanBeing_dressWithStringCalledOnce {
static
String expectedCloth;
@Mock
(
invocations =
1
)
protected
void
dress
(
String typeOfCloth){
Assert.assertEquals
(
"type of cloth"
, expectedCloth, typeOfCloth);
}
}
L'implémentation suivante satisfait le test :
protected
void
doSport
(
String typeOfSport){
if
(
"basket"
.equals
(
typeOfSport)){
dress
(
"short"
);
}
}
protected
void
dress
(
String typeOfClothes){
}
IV-B. Redéfinir un constructeur▲
Vraiment, ce n'est pas assez dit : ne faites rien d'autre qu'assigner des attributs dans un constructeur… Malheureusement, les constructeurs que l'on rencontre font souvent bien plus : ils initialisent des objets, invoquent des factory, font des appels statiques. C'est mal parce que pour poser un test sur un objet, nous sommes obligés de l'instancier… et donc de faire avec toute cette mécanique et gérer les effets de bord à chacun de nos tests.
JMockit permet de s'affranchir de ces constructeurs maudits grâce à la méthode $init.
IV-B-1. Avec JMockit Annotations▲
Nous avons un travailleur, et souhaitons implémenter sa façon de faire le dîner le lundi :
public
class
WorkingHuman extends
HumanBeing {
public
WorkingHuman
(
) {
}
protected
void
makeDinner
(
String dayOfWeek) {
}
}
Le lundi, c'est dur… Alors le travailleur fait chauffer une pizza. Une basique.
public
class
Pizza extends
Food {
public
final
static
String[] BASIC_INGREDIENTS =
new
String[]{
"cheese"
,"tomato"
}
;
public
Pizza
(
String...ingredients) {
}
}
Autrement dit, nous souhaitons vérifier que lorsqu'un travailleur fait le dîner (makeDinner) :
- le constructeur de Pizza soit appelé ;
- les paramètres du constructeur soient les ingrédients les plus basiques.
En test, cela donne :
@Test
public
void
testMakeDinner
(
) {
WorkingHuman human =
new
WorkingHuman
(
);
Mockit.setUpMocks
(
MockPizza_ConstructorCalledOnce.class
);
MockPizza_ConstructorCalledOnce.expectedIngredients =
Pizza.BASIC_INGREDIENTS;
// Method tested
human.makeDinner
(
"monday"
);
}
@MockClass
(
realClass =
Pizza.class
)
public
static
class
MockPizza_ConstructorCalledOnce {
static
String[] expectedIngredients;
// Can't be static !!!
Pizza it;
@Mock
(
invocations =
1
)
public
void
$init
(
String ... ingredients) {
assertThat
(
ingredients, is
(
equalTo
(
expectedIngredients)));
}
}
Mockit.setUpMocks : active les mocks passés en paramètres. Attention au -s à la fin de SetUpMocks (rien à voir avec SetUpMock).
@MockClass(realClass = Pizza.class) : définit un mock et la classe réelle qu'il remplace.
Variable d'instance it : permet d'avoir une référence sur l'objet construit, comme le constructeur ne peut rien renvoyer. C'est utile pour vérifier que l'objet est bien passé à une autre méthode par exemple. Attention à ne pas rendre la variable statique, car cela ne fonctionnerait pas.
@Mock(invocations = 1) : spécifie qu'une méthode doit être appelée une et une seule fois.
$init : méthode appelée à la place du constructeur, pour le mocker. N'oubliez pas le $ au début d'$init.
IV-B-2. Avec JMockit Core▲
C'est exactement la même méthode $init à implémenter. Si une référence sur l'objet instancié est nécessaire, une variable d'instance nommée it la contiendra de la même façon.
Par rapport à JMockit Annotations, il n'est pas possible de vérifier les invocations de méthodes. Si le constructeur n'est pas appelé, il n'y aura pas d'erreur… Par contre, si le constructeur est appelé avec les mauvais paramètres, ce sera détecté.
@Test
public
void
testMakeDinner_core
(
) {
WorkingHuman human =
new
WorkingHuman
(
);
Mockit.redefineMethods
(
Pizza.class
, MockPizza_ConstructorCore.class
);
MockPizza_ConstructorCore.expectedIngredients =
Pizza.BASIC_INGREDIENTS;
// Method tested
human.makeDinner
(
"monday"
);
}
public
static
class
MockPizza_ConstructorCore {
static
String[] expectedIngredients;
// Can't be static !!!
Pizza it;
public
void
$init
(
String ... ingredients) {
assertThat
(
ingredients, is
(
equalTo
(
expectedIngredients)));
}
}
En conséquence, JMockit Core est plus utile pour faire une fausse Pizza, pour avoir un bouchon, que pour effectuer des assertions.
L'auteur de JMockit signale sur son site qu'il y a trois avantages à utiliser $init pour mocker le constructeur, plutôt que d'avoir un constructeur de mock directement (new MockPizza(){}) :
- l'attribut it est accessible dans $init alors que dans un vrai constructeur, il ne serait pas initialisé avant la fin de la construction ;
- la méthode $init sera appelée sur l'instance du mock, même s'il a été défini dans le test alors qu'avec MockPizza, ce ne serait pas possible, car il est impossible d'appeler le constructeur d'une instance qui existe déjà ;
- étant une méthode, $init peut être statique, contrairement à un constructeur.
IV-C. Redéfinir un bloc statique▲
Les blocs statiques peuvent devenir un véritable cauchemar lorsque nous essayons d'introduire des tests sur du code existant. Le bloc étant statique, nous avons beau mocker, il est tout de même appelé. Avec JMockit, il devient possible de mocker aussi ces blocs et de les rendre caduques.
La classe Candidate contient un bloc d'initialisation statique.
public
class
Candidate
{
protected
static
List<
String>
minimumQualifications;
static
{
minimumQualifications =
new
ArrayList<
String>(
);
minimumQualifications.add
(
"typing"
);
minimumQualifications.add
(
"english speaking"
);
}
}
Nous allons écrire un test pour vérifier que le bloc statique est bien redéfini, en vérifiant que minimumQualifications est null. La méthode magique pour mocker l'initialiseur est la même pour Core et Annotations : public void $clinit.
IV-C-1. Avec JMockit Core▲
JMockit Core est plus approprié lorsque nous ne souhaitons rien vérifier, juste faire en sorte que l'initialisation statique ne vienne pas perturber notre mock.
@Test
public
void
testPizza_staticInitWithCore
(
) throws
Exception {
Mockit.redefineMethods
(
Candidate.class
, MockCandidate_staticCore.class
);
Candidate candidate =
new
Candidate
(
);
Assert.assertEquals
(
"qualifications mocked"
, null
, Candidate.minimumQualifications);
}
static
class
MockCandidate_staticCore {
void
$clinit
(
){
// no initialisation of qualifications
}
}
Mockit.redefineMethods définit la classe par laquelle il faut remplacer Candidate.class.
$clinit est appelée à la place du bloc statique de Candidate.
Le test passe : le bloc statique n'a effectivement pas été pris en compte.
IV-C-2. Avec JMockit Annotations▲
JMockit Annotation permet d'avoir des attentes particulières en plus : par exemple que le bloc statique est bien appelé.
@Test
public
void
testPizza_staticInitWithAnnotations
(
) throws
Exception {
Mockit.setUpMocks
(
MockCandidate_staticAnnotations.class
);
Candidate candidate =
new
Candidate
(
);
// To assert that the static initializer has been called
Assert.assertEquals
(
"qualifications mocked"
, null
, Candidate.minimumQualifications);
}
@MockClass
(
realClass =
Candidate.class
)
public
static
class
MockCandidate_staticAnnotations {
@Mock
(
invocations=
1
)
void
$clinit
(
){
// no initialisation of qualifications
}
}
Mockit.setUpMocks déclare le mock. C'est l'annotation sur la classe de mock qui fait le lien avec la classe mockée : @MockClass(realClass = XXX.class).
@Mock(invocations=1) vérifie que le bloc statique n'est appelé qu'une seule fois.
void $clinit() est appelée à la place du bloc statique de la classe mockée.
Le test passe lui aussi. Et il échoue si nous commentons le bloc statique dans la classe Candidate.
Que se passe-t-il s'il y a plusieurs blocs statiques ? Pas grand-chose en fait, car le compilateur Java fusionne les séquences des blocs statiques en un seul bloc « <clinit> » interne. Tous vos blocs seront donc mockés pareillement dans le $clinit que vous aurez redéfini.
IV-D. Créer une implémentation vide d'une interface▲
Il y a les objets que l'on souhaite mocker parce que l'on en attend des choses (que telle méthode soit appelée, avec tel paramètre) et les objets que l'on souhaite simplement bouchonner, pour affranchir notre test unitaire des dépendances de l'objet testé.
La méthode newEmptyProxy de JMockit comblera les besoins de bouchon simple sur une interface.
Reprenons le tout premier exemple, en un peu plus enrichi :
public
void
doOperation
(
String value)
{
DatabaseLogger logger =
new
DatabaseLogger
(
);
FullMessage info =
logger.info
(
"starting operation : "
+
value);
// Execution de l'opération
// ....
// ....
// ....
logger.trace
(
"coucou"
);
}
Nous retournions null dans la méthode logger.info de notre mock, pour garder les choses simples. Néanmoins, nous pouvons avoir besoin de retourner un objet réel, si nous voulons tester que l'objet retourné est passé en paramètre d'une autre méthode par exemple.
FullMessage est une interface, que FullMessageImpl implémente. Mais son instanciation coûte cher (le constructeur a des accès en base ou lit un fichier) et nous n'avons pas de vérification particulière à faire dessus. Nous aimerions retourner un objet plus simple que FullMessageImpl, une espèce de coquille vide. C'est possible manuellement, en créant un sous-type de l'interface.
@Test
public
void
testDoOperation_nominalCase_withManualProxy
(
)
{
MyService service =
new
MyService
(
);
// Definition du mock
Mockit.redefineMethods
(
DatabaseLogger.class
, MockDatabaseLoggerWithObjectToReturn.class
);
final
String param =
"anything"
;
MockDatabaseLoggerWithObjectToReturn.expectedParam =
"starting operation : "
+
param;
MockDatabaseLoggerWithObjectToReturn.objectToReturn =
new
FullMessage
(
) {
@Override
public
void
helloTata
(
)
{
// TODO Auto-generated method stub
}
@Override
public
void
helloTiti
(
)
{
// TODO Auto-generated method stub
}
}
;
service.doOperation
(
param);
}
public
static
class
MockDatabaseLoggerWithObjectToReturn
{
public
static
String expectedParam;
public
static
Object objectToReturn;
public
Object info
(
String sentence)
{
System.out.println
(
"MOCK DatabaseLogger.info()"
);
assertThat
(
sentence, is
(
expectedParam));
return
objectToReturn;
}
}
Le gros désavantage est l'encombrement de la solution : le code est brouillé. Il faut implémenter toutes les interfaces et corriger tous les tests si un jour une méthode est ajoutée. JMockit propose une syntaxe plus élégante :
@Test
public
void
testDoOperation_nominalCase_withEmptProxy
(
)
{
MyService service =
new
MyService
(
);
// Definition du mock
Mockit.redefineMethods
(
DatabaseLogger.class
, MockDatabaseLoggerWithEmptyProxy.class
);
final
String param =
"anything"
;
MockDatabaseLoggerWithEmptyProxy.expectedParam =
"starting operation : "
+
param;
MockDatabaseLoggerWithEmptyProxy.objectToReturn =
Mockit.newEmptyProxy
(
FullMessage.class
);
service.doOperation
(
param);
}
Nous avons maintenant une référence sur l'objet retourné et pouvons faire des vérifications dessus.
V. JMockit Expectations▲
JMockit Annotations et JMockit Core imposent la définition d'une classe de mock supplémentaire. JMockit Expectations permet d'effectuer des expectations sans redéfinir de classes de mock. En contrepartie, l'API a d'autres limitations comme l'impossibilité de mocker un constructeur ou un bloc statique. Elle est aussi plus stricte : l'ordre des appels de méthodes compte.
V-A. Mocker une méthode « simple »▲
Nous souhaitons vérifier que la méthode live de HumanBeing invoque la méthode sleep().
public
class
HumanBeing extends
AbstractLivingBeing
{
public
void
live
(
)
{
}
}
Nous écrivons le test correspondant :
@Test
public
void
testLive_nominalCase
(
) {
HumanBeing being =
new
HumanBeing
(
);
new
Expectations
(
true
)
{
// "Premier bloc" :
// Mettre ici les classes que l'on souhaite mocker,
// ainsi que les méthodes dont on veut vérifier l'appel dans l'annotation
@Mocked
(
methods={
"sleep"
}
)
HumanBeing mock;
{
// "Deuxieme bloc" :
// Mettre ici les expectations c'est-à-dire les appels attendus. Les méthodes attendues doivent
// aussi apparaitre dans l'annotation pour être vérifiées.
mock.sleep
(
);
}
}
;
// La methode testee
being.live
(
);
}
Le test échoue pour l'instant, comme nous n'avons pas encore implémenté le code. Notez les doubles accolades après new Expectations :
Le premier bloc permet de déclarer les objets devant être mockés. L'annotation @Mocked nous informe que c'est le type HumanBeing que nous mockons. Le paramètre methods={« sleep »} signifie que nous avons des attentes sur la méthode sleep.
Le deuxième bloc définit les expectations : quels paramètres sont passés, dans quel ordre les méthodes sont appelées ?
Après implémentation du code, le test passe :
public
void
live
(
)
{
sleep
(
);
}
Remarquez que le test passe toujours avec l'appel de eat() en plus, car nous n'avons pas défini d'attentes particulières sur cette méthode.
public
void
live
(
)
{
sleep
(
);
eat
(
);
}
V-B. Mocker une méthode statique▲
Le principe reste le même pour mocker une méthode statique. Même si elle n'est jamais utilisée, il faut déclarer une instance dans le premier bloc pour signaler que la classe est mockée.
Après sleep, c'est Entertainer.displayTvShow qui doit être appelé. Commençons déjà par déclarer le mock sur la méthode statique :
@Test
public
void
testLive_demoStaticMethod
(
) {
HumanBeing being =
new
HumanBeing
(
);
new
Expectations
(
true
)
{
// "Premier bloc" :
// Mettre ici les classes que l'on souhaite mocker,
// ainsi que les méthodes dont on veut vérifier l'appel dans l'annotation
@Mocked
(
methods={
"sleep"
}
)
HumanBeing mock;
@Mocked
(
methods={
"displayTvShow"
}
)
Entertainer entertainer;
{
// "Deuxieme bloc" :
// Mettre ici les expectations, ie les appels attendus. Les méthodes attendues doivent
// aussi apparaitre dans l'annotation pour être vérifiées.
mock.sleep
(
);
}
}
;
// La methode testee
being.live
(
);
}
Le test passe toujours. Maintenant, nous voulons vérifier l'appel de displayTvShow après l'appel de sleep :
new
Expectations
(
true
)
{
// "Premier bloc" :
// Mettre ici les classes que l'on souhaite mocker,
// ainsi que les méthodes dont on veut vérifier l'appel dans l'annotation
@Mocked
(
methods={
"sleep"
}
)
HumanBeing mock;
@Mocked
(
methods={
"displayTvShow"
}
)
Entertainer entertainer;
{
// "Deuxieme bloc" :
// Mettre ici les expectations, ie les appels attendus. Les méthodes attendues doivent
// aussi apparaitre dans l'annotation pour être vérifiées.
mock.sleep
(
);
Entertainer.displayTvShow
(
);
}
}
;
Le test échoue bien. Nous pouvons modifier l'implémentation pour le faire passer :
public
void
live
(
)
{
sleep
(
);
Entertainer.displayTvShow
(
);
}
V-C. Mocker toutes les méthodes d'une classe▲
Si l'annotation Mocked n'a pas de valeur, alors toutes les méthodes de la classe HumanBeing sont mockées.
new
Expectations
(
true
)
{
@Mocked
HumanBeing mock;
{
Nous avons le même comportement sans mettre d'annotation du tout, tant que nous sommes dans le premier bloc des Expectations.
new
Expectations
(
true
)
{
HumanBeing mock;
{
V-D. Mocker toutes les méthodes d'une classe sauf certaines▲
Or nous ne voulons pas tout mocker de la classe HumanBeing, surtout pas la méthode live puisque c'est elle que nous testons… L'attribut inverse de @Mocked permet de dire « je veux mocker toutes les méthodes de la classe, SAUF celle que je spécifie ». Prenons l'implémentation de la méthode liveNoTv :
public
void
liveNoTv
(
)
{
sleep
(
);
chat
(
);
goToWork
(
);
eat
(
);
}
Plutôt que d'énumérer toutes les méthodes dans l'annotation, nous utilisons l'attribut inverse :
@Test
public
void
testLiveNoTv_allMockedExcept
(
) {
HumanBeing being =
new
HumanBeing
(
);
new
Expectations
(
true
)
{
@Mocked
(
methods={
"liveNoTv"
}
,inverse=
true
)
HumanBeing mock;
{
mock.sleep
(
);
mock.goToWork
(
);
mock.eat
(
);
}
}
;
// La methode testee
being.liveNoTv
(
);
}
V-E. Définir l'expectation d'une méthode privée▲
L'être humain peut parler : ne nous privons pas de bavarder. Par contre, pas sûr que nos enfants sachent le faire dès la naissance. Cette méthode sera donc privée.
public
void
liveAndChat
(
)
{
eat
(
);
chat
(
);
}
private
void
chat
(
)
{
// let's talk
}
invoke permet de mocker des méthodes privées avec l'introspection :
@Test
public
void
testLiveAndChat_demoPrivateMethod
(
) {
HumanBeing being =
new
HumanBeing
(
);
new
Expectations
(
true
)
{
@Mocked
(
methods={
"eat"
,"chat"
}
)
HumanBeing mock;
{
mock.eat
(
);
invoke
(
mock, "chat"
);
}
}
;
// La methode testee
being.liveAndChat
(
);
}
Le gros désavantage est que le nom de la méthode est passé en paramètre : si son nom change, le refactoring ne le prendra pas en compte. Le fait de lancer les tests régulièrement limite le problème, mais cela reste assez contraignant.
Certains préféreront simplement changer la portée de la méthode en protected. D'autres argueront que la méthode privée est indirectement testée par la méthode publique d'une part et surtout qu'il s'agit d'un détail d'implémentation qui ne devrait pas casser des tests.
Personnellement, je n'aime pas le code redondant et si ma méthode privée est privée justement pour éviter des redondances, je préfère tester qu'elle soit appelée plutôt que d'avoir du code dupliqué dans mon test. Si par contre ma méthode est privée pour avoir un nom plus lisible par exemple, alors effectivement tester uniquement la méthode publique me semble plus pertinent.
V-F. Vérifier qu'une méthode est appelée n fois▲
Et si j'ai envie de bavarder également avant de manger ?
public
void
liveAndChatMore
(
)
{
chat
(
);
eat
(
);
chat
(
);
}
Il y a deux façons de formaliser le test. L'expectation sur chat est spécifiée deux fois :
@Test
public
void
testLiveAndChat_demoRepeat
(
) {
HumanBeing being =
new
HumanBeing
(
);
new
Expectations
(
true
)
{
@Mocked
(
methods={
"eat"
,"chat"
}
)
HumanBeing mock;
{
invoke
(
mock, "chat"
);
mock.eat
(
);
invoke
(
mock, "chat"
);
}
}
;
// La methode testee
being.liveAndChatMore
(
);
}
Si les deux appels se suivaient, nous aurions pu utiliser la méthode repeats, pour dire combien de fois une méthode doit être appelée.
invoke
(
mock, "chat"
);repeats
(
2
);
Comme l'ordre des invocations compte dans JMockit Expectations, repeats ne convient pas ici.
V-G. Vérifier les arguments passés à une méthode▲
Travailler, bavarder, c'est bien, il faut aussi se bouger un peu ! L'humain peut-il courir ? Testons.
@Test
public
void
testLiveAndChat_demoCheckParam
(
) {
HumanBeing being =
new
HumanBeing
(
);
new
Expectations
(
true
)
{
@Mocked
(
methods={
"sleep"
,"doSport"
}
)
HumanBeing mock;
{
mock.sleep
(
);
mock.doSport
(
"running"
);
}
}
;
// La methode testee
being.liveAndMove
(
);
}
Le test affiche l'erreur suivante :
java.lang.AssertionError: Parameter 0 of void fr.fchabanois.tutorial.jmockit.HumanBeing#doSport(String) expected "running", got "foot"
at fr.fchabanois.tutorial.jmockit.HumanBeing.doSport(HumanBeing.java)
at fr.fchabanois.tutorial.jmockit.HumanBeing.liveAndMove(HumanBeing.java:22)
at fr.fchabanois.tutorial.jmockit.HumanBeingTest.testLiveAndChat_demoCheckParam(HumanBeingTest.java:164)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:45)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
Apparemment, l'humain préfère le foot. Après tout, tant que c'est du sport, c'est bien. Nous assouplissons notre test : le sport que tu voudras faire, tu pourras grâce à withAny:
@Test
public
void
testLiveAndChat_demoCheckParam
(
) {
HumanBeing being =
new
HumanBeing
(
);
new
Expectations
(
true
)
{
@Mocked
(
methods={
"sleep"
,"doSport"
}
)
HumanBeing mock;
{
mock.sleep
(
);
mock.doSport
(
withAny
(
new
String
(
)));
}
}
;
// La methode testee
being.liveAndMove
(
);
}
V-H. Mocker l'objet de retour d'une méthode▲
La méthode returns permet de mocker l'objet de retour d'une méthode :
protected
String give
(
String something)
{
return
something;
}
Typiquement, l'intérêt est de vérifier que cet objet est passé en paramètre d'une autre méthode. Par exemple, la chaîne retournée par give doit être passée en paramètre à say.
public
void
liveWithLove
(
) {
sleep
(
);
String string =
give
(
"a kiss"
);
say
(
string);
}
Voici le test au complet :
@Test
public
void
testLiveWithLove_demoReturn
(
) {
HumanBeing being =
new
HumanBeing
(
);
final
String aHug =
"a hug"
;
new
Expectations
(
true
)
{
@Mocked
(
methods={
"sleep"
,"give"
,"say"
}
)
HumanBeing mock;
{
mock.sleep
(
);
mock.give
(
"a kiss"
);returns
(
aHug);
mock.say
(
aHug);
}
}
;
// La methode testee
being.liveWithLove
(
);
}
VI. Conclusion▲
JMockit est un framework de test prometteur, puissant, simple et permet de sécuriser même du code « sale ». Je n'ai à mon souvenir jamais rencontré de code que je ne pouvais pas tester grâce à JMockit.
Cette puissance est un peu son défaut. Le fait que JMockit permette de tout mocker peut assez ironiquement nous encourager à écrire du code « intestable », sans nous en rendre compte… On en rajoute encore et encore et c'est l'application qui devient « intestable ». Est-ce que c'est grave, puisqu'elle est testée ?
Oui. C'est un peu comme si l'on mangeait des frites au Nutella à longueur de journée (bon appétit !), parce qu'on sait qu'au pire, la liposuccion existe. JMockit est un remède et il vaut mieux prévenir que guérir. De plus :
- le fait que le remède soit quasiment « unique » fait qu'on en est dépendant. Impossible de changer de framework de test. C'est JMockit et point barre, même si le framework devient une usine à gaz incompréhensible. Tous les développeurs sont obligés d'apprendre cette API, sinon il n'y pas de tests ;
- écrire du code testable s'apprend et prend du temps. C'est une bonne pratique qui ne devrait pas être spécifique à un langage. En Java, poser des tests sur de code sale est possible, mais c'est vraiment une chance, un cas particulier. Une chance que vous n'aurez peut-être pas dans votre prochain poste. Si vous voulez être en mesure de poser des tests dans n'importe quel langage ou ne serait-ce que sur du Java 1.4, il faut savoir écrire du code testable et pas compter sur un framework magique.
Le code de l'application devrait rester testable avec n'importe quoi, même des mocks maison. Pour que ce soit le cas, j'utilise par défaut un framework de mocks traditionnel et programme en Test Driven Development. Le TDD nous incite naturellement à écrire du code testable. Ce n'est que sur du code existant, quand je dois poser des tests après coup, que je n'y arrive pas avec les moyens « normaux » et que je ne peux pas refactorer que j'ai recours à JMockit. Pour des raisons de lisibilité, j'utilise principalement les Expectations de JMockit.
Pour finir, je rappelle que ce tutoriel n'est qu'un petit aperçu du framework. Il rassemble les besoins que j'ai eus pendant mes développements, mais vous en avez peut-être d'autres. Rogerio Liesenfeld travaillant très activement sur ce framework, n'hésitez pas à jeter un œil sur les évolutions qu'il y apporte jour après jour.