IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Tester du code intestable avec JMockit

JMockit est un framework de mocks pour les tests unitaires. En plus de proposer les fonctionnalités habituelles de mocking, il permet de poser des tests sur du code dit intestable. Absolument tout est mockable : les méthodes statiques, les initialiseurs statiques, les constructeurs et même les méthodes privées. 4 commentaires Donner une note à l´article (4.5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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.

 
Sélectionnez
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.

 
Sélectionnez
public class MyClass
{
  private MyService service;

  public MyClass(MyService service)
  {
    this.service = service;
    //whatever
  }
}
Extrait du test
Sélectionnez
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.

Le site officiel

Documentation d'installation officielle

Le tutoriel officiel

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 :

Artefact jmockit
Sélectionnez
<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 :

 
Sélectionnez
-javaagent:C:\.m2\repository\mockit\jmockit\0.993\jmockit-0.993.jar

Le recours à une variable de substitution facilite les montées de version :

 
Sélectionnez
-javaagent:${JMOCKIT_JAR}

III-A-2. Avec Maven

 
Sélectionnez
<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.

 
Sélectionnez
@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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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() :

 
Sélectionnez
    public void trace(String sentence) {
      System.out.println("DatabaseLogger.trace - " + sentence);
    }

Modifions la classe de service pour invoquer trace.

 
Sélectionnez
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");
  }

}
La console affiche...
Sélectionnez
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().

 
Sélectionnez
  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.

 
Sélectionnez
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 :

 
Sélectionnez
@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 :

 
Sélectionnez
protected void doSport(String typeOfSport){
}

protected void dress(String typeOfClothes){
    
}

Le test deviendrait :

 
Sélectionnez
@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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
@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é.

Version avec JMockit Core
Sélectionnez
@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.

 
Sélectionnez
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.

 
Sélectionnez
@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é.

 
Sélectionnez
@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 :

 
Sélectionnez
  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.

 
Sélectionnez
    @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 :

 
Sélectionnez
  @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().

 
Sélectionnez
public class HumanBeing extends AbstractLivingBeing
{
  public void live()
  {

  }
}

Nous écrivons le test correspondant :

 
Sélectionnez
    @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 :

 
Sélectionnez
  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.

 
Sélectionnez
  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 :

 
Sélectionnez
@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 :

 
Sélectionnez
      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 :

 
Sélectionnez
  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.

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
  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 :

 
Sélectionnez
@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.

 
Sélectionnez
public void liveAndChat()
  {
    eat();
    chat();
  }
  
  private void chat()
  {
    // let's talk 
  }

invoke permet de mocker des méthodes privées avec l'introspection :

 
Sélectionnez
    @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 ?  

 
Sélectionnez
  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 :

 
Sélectionnez
    @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.

 
Sélectionnez
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.

 
Sélectionnez
    @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 :

 
Sélectionnez
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:

 
Sélectionnez
    @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 :

 
Sélectionnez
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.

 
Sélectionnez
  public void liveWithLove() {
    sleep();
    String string = give("a kiss");
    say(string);
  }

Voici le test au complet :

 
Sélectionnez
    @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.

VII. Remerciements

Merci à Christophe et Ricky81 pour leurs conseils. Un grand merci également à Wachter pour sa relecture attentive.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Copyright © 2009 Florence Chabanois. Aucune reproduction, même partielle, ne peut être faite de ce site ni 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. Droits de diffusion permanents accordés à Developpez LLC.