EasyMock 3.2 Readme

Documentation de la version 3.2 (2013-07-11)
© 2001-2013 OFFIS, Tammo Freese, Henri Tremblay.

Documentation traduite originellement de l'anglais par Alexandre de Pellegrin. Maintenue par Henri Tremblay.

EasyMock est une librairie fournissant un moyen simple d'utiliser des Mock Objects pour une interface ou classe donnée. EasyMock est disponible sous license Apache 2.

Les Mock Objects simulent le comportement du code métier et sont capables de vérifier s'il est utilisé comme prévu. Les classes métier peuvent être testées de façon isolée en simulant leurs objets liés par des Mock Objects.

Écrire et maintenir des Mock Objects est souvent une tâche pénible et source d'erreurs. EasyMock génère les Mock Objects dynamiquement - pas besoin de les écrire, pas de code généré!

Avantages d'EasyMock

Environnement Requis

Installation

Avec Maven

EasyMock est disponible dans le référentiel central de Maven. Ajoutez la dépendance suivante à votre pom.xml:
    <dependency>
      <groupId>org.easymock</groupId>
      <artifactId>easymock</artifactId>
      <version>3.2</version>
      <scope>test</scope>
    </dependency>
Vous pouvez, bien évidemment, n'importe quel outil de gestion de dépendances compatible avec le référentiel Maven.

Manuellement

Utilisation

La plupart des éléments d'un logiciel ne fonctionnent pas de manière isolée mais en collaboration avec d'autres éléments (objets liés) pour effectuer leur tâche. Dans beaucoup de cas, nous ne nous soucions pas d'utiliser des objets liés pour nos tests unitaires du moment que nous avons confiance en eux. Si ce n'est pas le cas, les Mock Objects peuvent nous aider à tester unitairement de façon isolée. Les Mock Objects remplacent les objets liés de l'élément testé.

Les exemples suivants utilisent l'interface Collaborator:

package org.easymock.samples;

public interface Collaborator {
    void documentAdded(String title);
    void documentChanged(String title);
    void documentRemoved(String title);
    byte voteForRemoval(String title);
    byte[] voteForRemovals(String[] title);
}

Les implémentations de cette interface sont des objets liés (des listeners dans ce cas) à la classe nommée ClassUnderTest:

public class ClassUnderTest {

    private Collaborator listener;
    // ...
    public void setListener(Collaborator listener) {
        this.listener = listener;
    }
    public void addDocument(String title, byte[] document) { 
        // ... 
    }
    public boolean removeDocument(String title) {
        // ... 
    }
    public boolean removeDocuments(String[] titles) {
        // ... 
    }
}

Le code de la classe et de l'interface est disponible dans le package org.easymock.samples dans easymock-3.2-samples.jar inclue dans la livraison d'EasyMock.

Les exemples qui suivent supposent que vous êtes familier avec le framework de test JUnit. Bien que les tests montrés ici utilisent JUnit 4, vous pouvez également utiliser JUnit 3 ou TestNG.

Votre premier Mock Object

Nous allons maintenant construire un cas de test et jouer avec pour comprendre les fonctionnalités du package EasyMock. Le fichier easymock-3.2-samples.jar contient une version modifiée de ce test. Notre premier test devra vérifier que la suppression d'un document non existant ne doit pas provoquer la notification de l'objet lié. Voici le test dans la définition du Mock Object:

package org.easymock.samples;

import org.junit.*;

public class ExampleTest {

    private ClassUnderTest classUnderTest;
    private Collaborator mock;

    @Before
    public void setUp() {
        classUnderTest = new ClassUnderTest();
        classUnderTest.setListener(mock);
    }

    @Test
    public void testRemoveNonExistingDocument() {    
        // This call should not lead to any notification
        // of the Mock Object: 
        classUnderTest.removeDocument("Does not exist");
    }
}

Pour beaucoup de tests utilisant EasyMock, nous avons uniquement besoin de l'import statique des méthodes de la classe org.easymock.EasyMock. Cette classe est la seule non interne et non dépréciée d'EasyMock 2.

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

public class ExampleTest {

    private ClassUnderTest classUnderTest;
    private Collaborator mock;
    
}    

Pour obtenir un Mock Object, il faut:

  1. créer un Mock Object pour l'interface à simuler,
  2. enregistrer le comportement attendu, puis
  3. basculer le Mock Object à l'état 'replay'.

Voici le premier exemple:

    @Before
    public void setUp() {
        mock = createMock(Collaborator.class); // 1
        classUnderTest = new ClassUnderTest();
        classUnderTest.addListener(mock);
    }

    @Test
    public void testRemoveNonExistingDocument() {
        // 2 (we do not expect anything)
        replay(mock); // 3
        classUnderTest.removeDocument("Does not exist");
    }

Après activation à l'étape 3, mock est un Mock Object de l'interface Collaborator qui n'attend aucun appel. Cela signifie que si nous changeons notre ClassUnderTest pour appeler n'importe quelle méthode de l'interface, le Mock Object lèvera une AssertionError:

java.lang.AssertionError: 
  Unexpected method call documentRemoved("Does not exist"):
    at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)
    at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)
    at $Proxy0.documentRemoved(Unknown Source)
    at org.easymock.samples.ClassUnderTest.notifyListenersDocumentRemoved(ClassUnderTest.java:74)
    at org.easymock.samples.ClassUnderTest.removeDocument(ClassUnderTest.java:33)
    at org.easymock.samples.ExampleTest.testRemoveNonExistingDocument(ExampleTest.java:24)
    ...

Utiliser les annotations

Depuis EasyMock 3.2, il est maintenant possible de créer un Mock Object à l'aide d'annotations. C'est une méthode à la fois simple et rapide pour créer vos Mock Objects et les injecter à la classe testée. Reprenons l'exemple ci-dessus en utilisant les annotations:

import static org.easymock.EasyMock.*;

import org.easymock.EasyMockRunner;
import org.easymock.TestSubject;
import org.easymock.Mock;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(EasyMockRunner.class)
public class ExampleTest {

    @TestSubject
    private ClassUnderTest classUnderTest = new ClassUnderTest(); // 2

    @Mock
    private Collaborator mock; // 1

    @Test
    public void testRemoveNonExistingDocument() {
        replay(mock);
        classUnderTest.removeDocument("Does not exist");
    }
}

Le mock est instancié par le runner à l'étape 1. Il est ensuite assigné, toujours par le runner, au champs listener de classUnderTest à l'étape 2. La méthode setUp n'est donc plus nécessaire étant donné que toute l'initialisation a déjà été effectuée par le runner.

Ajouter un comportement

Écrivons un second test. Si un document est ajouté à la classe testée, nous nous attendons à un appel à mock.documentAdded() sur le Mock Object avec le titre du document en argument:

    @Test
    public void testAddDocument() {
        mock.documentAdded("New Document"); // 2
        replay(mock); // 3
        classUnderTest.addDocument("New Document", new byte[0]); 
    }

Aussi, dans l'étape d'enregistrement (avant d'appeler replay), le Mock Object ne se comporte pas comme un Mock Object mais enregistre les appels de méthode. Après l'appel à replay, il se comporte comme un Mock Object, vérifiant que les appels de méthode attendus ont bien lieu.

Si classUnderTest.addDocument("New Document", new byte[0]) appelle la méthode attendue avec un mauvais argument, le Mock Object lèvera une AssertionError:

java.lang.AssertionError: 
  Unexpected method call documentAdded("Wrong title"):
    documentAdded("New Document"): expected: 1, actual: 0
    at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)
    at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)
    at $Proxy0.documentAdded(Unknown Source)
    at org.easymock.samples.ClassUnderTest.notifyListenersDocumentAdded(ClassUnderTest.java:61)
    at org.easymock.samples.ClassUnderTest.addDocument(ClassUnderTest.java:28)
    at org.easymock.samples.ExampleTest.testAddDocument(ExampleTest.java:30)
    ...

Tous les appels attendus n'ayant pas eu lieu sont montrés, ainsi que tous les appels faits alors qu'ils étaient non attendus (aucun dans notre cas). Si l'appel à la méthode est effectué trop de fois, le Mock Object le signale également:

java.lang.AssertionError: 
  Unexpected method call documentAdded("New Document"):
    documentAdded("New Document"): expected: 1, actual: 2
    at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)
    at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)
    at $Proxy0.documentAdded(Unknown Source)
    at org.easymock.samples.ClassUnderTest.notifyListenersDocumentAdded(ClassUnderTest.java:62)
    at org.easymock.samples.ClassUnderTest.addDocument(ClassUnderTest.java:29)
    at org.easymock.samples.ExampleTest.testAddDocument(ExampleTest.java:30)
    ...

Vérifier le comportement

Il y a un type d'erreur dont nous ne nous sommes pas préoccupés jusqu'à présent: si nous décrivons un comportement, nous voulons vérifier qu'il est bien respecté. Le test qui suit passe si une méthode du Mock Object est appelée. Pour vérifier cela, nous devons appeler verify(mock):

    @Test
    public void testAddDocument() {
        mock.documentAdded("New Document"); // 2 
        replay(mock); // 3
        classUnderTest.addDocument("New Document", new byte[0]);
        verify(mock);
    }

Si la méthode du Mock Object n'est pas appelée, l'exception suivante sera levée :

java.lang.AssertionError: 
  Expectation failure on verify:
    documentAdded("New Document"): expected: 1, actual: 0
    at org.easymock.internal.MocksControl.verify(MocksControl.java:70)
    at org.easymock.EasyMock.verify(EasyMock.java:536)
    at org.easymock.samples.ExampleTest.testAddDocument(ExampleTest.java:31)
    ...

Le message de l'exception liste tous les appels attendus qui n'ont pas eu lieu.

Attendre un nombre explicite d'appels

Jusqu'à maintenant, nos tests ont été faits uniquement sur un seul appel de méthode. Le test suivant vérifiera que l'ajout d'un document déjà existant déclenche l'appel à mock.documentChanged() avec l'argument approprié. Pour en être certain, nous vérifions cela trois fois (après tout, c'est un exemple ;-)):

    @Test
    public void testAddAndChangeDocument() {
        mock.documentAdded("Document");
        mock.documentChanged("Document");
        mock.documentChanged("Document");
        mock.documentChanged("Document");
        replay(mock);
        classUnderTest.addDocument("Document", new byte[0]);
        classUnderTest.addDocument("Document", new byte[0]);
        classUnderTest.addDocument("Document", new byte[0]);
        classUnderTest.addDocument("Document", new byte[0]);
        verify(mock);
    }

Afin d'éviter la répétition de mock.documentChanged("Document"), EasyMock fournit un raccourci. Nous pouvons spécifier le nombre d'appel avec la méthode times(int times) sur l'objet retourné par expectLastCall(). Le code ressemble alors à cela:

    @Test
    public void testAddAndChangeDocument() {
        mock.documentAdded("Document");
        mock.documentChanged("Document");
        expectLastCall().times(3);
        replay(mock);
        classUnderTest.addDocument("Document", new byte[0]);
        classUnderTest.addDocument("Document", new byte[0]);
        classUnderTest.addDocument("Document", new byte[0]);
        classUnderTest.addDocument("Document", new byte[0]);
        verify(mock);
    }

Si la méthode est appelée un trop grand nombre de fois, une exception sera levée nous indiquant que la méthode a été appelée trop de fois. L'erreur est levée immédiatement après le premier appel dépassant la limite:

java.lang.AssertionError: 
  Unexpected method call documentChanged("Document"):
    documentChanged("Document"): expected: 3, actual: 4
	at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)
	at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)
	at $Proxy0.documentChanged(Unknown Source)
	at org.easymock.samples.ClassUnderTest.notifyListenersDocumentChanged(ClassUnderTest.java:67)
	at org.easymock.samples.ClassUnderTest.addDocument(ClassUnderTest.java:26)
	at org.easymock.samples.ExampleTest.testAddAndChangeDocument(ExampleTest.java:43)
    ...

S'il y a trop peu d'appels, verify(mock) lève une AssertionError:

java.lang.AssertionError: 
  Expectation failure on verify:
    documentChanged("Document"): expected: 3, actual: 2
	at org.easymock.internal.MocksControl.verify(MocksControl.java:70)
	at org.easymock.EasyMock.verify(EasyMock.java:536)
	at org.easymock.samples.ExampleTest.testAddAndChangeDocument(ExampleTest.java:43)
    ...

Spécifier des valeurs de retour

Pour spécifier des valeurs de retour, nous encapsulons l'appel attendu dans expect(T value) et spécifions la valeur de retour avec la méthode andReturn(Object returnValue) sur l'objet retourné par expect(T value).

Prenons par exemple la vérification du workflow lors de la suppression d'un document. Si ClassUnderTest fait un appel pour supprimer un document, il doit demander aux objets liés de voter pour cette suppression par appel à byte voteForRemoval(String title). Une réponse positive approuve la suppression. Si la somme de toutes les réponses est positive, alors le document est supprimé et l'appel à documentRemoved(String title) est effectué sur les objets liés:

    @Test
    public void testVoteForRemoval() {
        mock.documentAdded("Document");   // expect document addition
        // expect to be asked to vote for document removal, and vote for it
        expect(mock.voteForRemoval("Document")).andReturn((byte) 42);
        mock.documentRemoved("Document"); // expect document removal
        replay(mock);
        classUnderTest.addDocument("Document", new byte[0]);
        assertTrue(classUnderTest.removeDocument("Document"));
        verify(mock);
    }

    @Test
    public void testVoteAgainstRemoval() {
        mock.documentAdded("Document");   // expect document addition
        // expect to be asked to vote for document removal, and vote against it
        expect(mock.voteForRemoval("Document")).andReturn((byte) -42);
        replay(mock);
        classUnderTest.addDocument("Document", new byte[0]);
        assertFalse(classUnderTest.removeDocument("Document"));
        verify(mock);
    }

Le type de la valeur de retour est vérifié à la compilation. Par exemple, le code suivant ne compilera pas du fait que le type fourni ne correspond au type retourné par la méthode:

    expect(mock.voteForRemoval("Document")).andReturn("wrong type");

Au lieu d'appeler expect(T value) pour récupérer l'objet auquel affecter une valeur de retour, nous pouvons aussi utiliser l'objet retourné par expectLastCall(). Ainsi, au lieu de

    expect(mock.voteForRemoval("Document")).andReturn((byte) 42);

nous pouvons écrire

    mock.voteForRemoval("Document");
    expectLastCall().andReturn((byte) 42);

Ce type d'écriture doit uniquement être utilisé si la ligne est trop longue car il n'inclut pas la vérification du type à la compilation.

Travailler avec les exceptions

Afin de spécifier les exceptions (plus précisément: les Throwables) devant être levées, l'objet retourné par expectLastCall() et expect(T value) fournit la méthode andThrow(Throwable throwable). Cette méthode doit être appelée durant l'étape d'enregistrement après l'appel au Mock Object pour lequel le Throwable doit être levé.

Les exception non "checkées" (comme RuntimeException, Error ainsi que toutes leurs sous classes) peuvent être levées de n'importe quelle méthode. Les exceptions "checkées" ne doivent être levées que pour méthodes où cela est prévu.

Créer des valeurs de retour ou des exceptions

Parfois, nous voulons que notre Mock Object retourne une valeur ou lève une exception créée au moment de l'appel. Depuis la version 2.2 d'EasyMock, l'objet retourné par expectLastCall() et expect(T value) fournit la méthode andAnswer(IAnswer answer) permettant de spécifier une implémentation de l'interface IAnswer utilisée pour créer une valeur de retour ou une exception.

Au sein d'IAnswer, les arguments passés lors de l'appel au mock sont disponibles via EasyMock.getCurrentArguments(). Si vous utilisez cela, les refactorings du type réorganisation de l'ordre des arguments briseront vos tests. Vous êtes prévenu.

Une alternative à IAnswer sont les méthodes andDelegateTo et andStubDelegateTo. Elles permettent de déléguer un appel à une implémentation concrète de l'interface "mockées" et qui fournira la valeur de retour. L'avantage est que les paramètres normalement récupéré avec EasyMock.getCurrentArguments() pour IAnswer sont maintenant passés à la méthode de l'implémentation concrète. Ça supporte donc le refactoring. Le désavantage est qu'il faut fournir une implémentation... ce qui resemble un peu à faire un mock à la main. Ce que vous tentez d'éviter en utilisant EasyMock. Il peut aussi être pénible d'implémenter l'interface si celle-ci à beaucoup de méthodes. Finalement, le type de l'implémentation ne peut être vérifié statiquement par rapport au type du Mock Object. Si pour une quelconque raison, la class concrète n'implémente plus la méthode sur laquelle est délégué l'appel, vous aurez une exception lors de la phase de "replay". Ce cas devrait toutefois être assez rare.

Pour bien comprendre les deux options, voici un exemple:

    List<String> l = createMock(List.class);

    // andAnswer style
    expect(l.remove(10)).andAnswer(new IAnswer<String>() {
        public String answer() throws Throwable {
            return getCurrentArguments()[0].toString();
        }
    });

    // andDelegateTo style
    expect(l.remove(10)).andDelegateTo(new ArrayList<String>() {
        @Override
        public String remove(int index) {
            return Integer.toString(index);
        }
    });

Changer de comportement sur le même appel de méthode

Il est également possible de spécifier un changement de comportement pour une méthode. Les méthodes times, andReturn et andThrow peuvent être chaînées. Comme exemple, nous définissons voteForRemoval("Document") pour

    expect(mock.voteForRemoval("Document"))
        .andReturn((byte) 42).times(3)
        .andThrow(new RuntimeException(), 4)
        .andReturn((byte) -42);

Être plus permissif sur le nombre d'appels

Afin d'être plus permissif sur le nombre d'appels attendus, des méthodes additionnelles peuvent être utilisées à la place de times(int count):

times(int min, int max)
pour attendre entre min and max appels,
atLeastOnce()
pour attendre au moins un appel, et
anyTimes()
pour attendre une quantité non définie d'appels.

Si aucun nombre d'appels n'est explicitement défini, alors seul un appel est attendu. Pour le définir explicitement, vous pouvez utiliser once() ou times(1).

Mocks stricts

Sur un Mock Object retourné par EasyMock.createMock(), l'ordre d'appel des méthodes n'est pas vérifié. Si vous souhaitez avoir un Mock Object 'strict' vérifiant cet ordre, utilisez EasyMock.createStrictMock().

Lorsqu'un appel inattendu à une méthode est fait sur un Mock Object 'strict', le message de l'exception contient les appels de méthode attendus à ce moment, suivi du premier appel en conflit. verify(mock) montre tous les appels de méthode manqués.

Activer/Désactiver la vérification de l'ordre d'appel des méthodes

Il est parfois nécessaire qu'un Mock Object vérifie l'ordre d'appel sur certains appels uniquement. Pendant la phase d'enregistrement, vous pouvez activer la vérification de l'ordre d'appel en utilisant checkOrder(mock, true) et la désactiver en utilisant checkOrder(mock, false).

Il y a deux différences entre un Mock Object 'strict' et un Mock Object 'normal':

  1. Un mock 'strict' a la vérification de l'ordre d'appel activé à la création.
  2. Un mock 'strict' a la vérification de l'ordre d'appel activé après un reset (voir Réutilisation d'un Mock Object).

Définir des comparateurs d'arguments pour plus de souplesse

Pour vérifier la correspondance à un appel de méthode prévu sur un Mock Object, les arguments de type Object sont comparés, par défaut, avec equals(). Cela peut introduire des problèmes. Considérons l'exemple suivant:

String[] documents = new String[] { "Document 1", "Document 2" };
expect(mock.voteForRemovals(documents)).andReturn(42);

Si la méthode est appelée avec un autre tableau ayant le même contenu, cela provoque une exception du fait que equals() compare l'identité des objets pour les tableaux:

java.lang.AssertionError: 
  Unexpected method call voteForRemovals([Ljava.lang.String;@9a029e):
    voteForRemovals([Ljava.lang.String;@2db19d): expected: 1, actual: 0
    documentRemoved("Document 1"): expected: 1, actual: 0
    documentRemoved("Document 2"): expected: 1, actual: 0
	at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)
	at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)
	at $Proxy0.voteForRemovals(Unknown Source)
	at org.easymock.samples.ClassUnderTest.listenersAllowRemovals(ClassUnderTest.java:88)
	at org.easymock.samples.ClassUnderTest.removeDocuments(ClassUnderTest.java:48)
	at org.easymock.samples.ExampleTest.testVoteForRemovals(ExampleTest.java:83)
    ...

Pour spécifier que seule l'égalité de tableau est nécessaire pour cet appel, utilisez la méthode aryEq, importée statiquement de la classe EasyMock:

String[] documents = new String[] { "Document 1", "Document 2" };
expect(mock.voteForRemovals(aryEq(documents))).andReturn(42);

Si vous souhaitez utiliser les comparateurs lors d'un appel, vous devez en utiliser pour chaque argument de la méthode appelée.

Voici quelques comparateurs prédéfinis disponible:

eq(X value)
Vérifie que la valeur reçue égale la valeur attendue. Disponible pour tous les types primitifs et objets.
anyBoolean(), anyByte(), anyChar(), anyDouble(), anyFloat(), anyInt(), anyLong(), anyObject(), anyObject(Class clazz), anyShort(), anyString()
Laisse passer n'importe quelle valeur. Disponible pour tous les types primitifs et objets.
eq(X value, X delta)
Vérifie que la valeur reçue égale la valeur attendue, plus ou moins un delta. Disponible pour les float et double.
aryEq(X value)
Vérifie que la valeur reçue égale la valeur attendue en s'appuyant sur Arrays.equals(). Disponible pour les tableaux d'objets et de types primitifs.
isNull(), isNull(Class clazz)
Vérifie que la valeur reçue est nulle. Disponible pour les objets.
notNull(), notNull(Class clazz)
Vérifie que la valeur reçue n'est pas nulle. Disponible pour les objets.
same(X value)
Vérifie que la valeur reçue est la même que la value attendue. Disponible pour les objets.
isA(Class clazz)
Vérifie que la valeur reçue est une instance de clazz ou d'une classe hérite ou implémente clazz. Disponible pour les objets.
lt(X value), leq(X value), geq(X value), gt(X value)
Vérifie que la valeur reçue est inférieure/inférieure ou égale/supérieure ou égale/supérieure à la valeur attendue. Disponible pour tous les types primitifs numériques et les implémentations de Comparable.
startsWith(String prefix), contains(String substring), endsWith(String suffix)
Vérifie que la valeur reçue commence par/contient/se termine par la valeur attendue. Disponible pour les Strings.
matches(String regex), find(String regex)
Vérifie que la valeur reçue/une sous-chaîne de la valeur reçue correspond à l'expression ré. Disponible pour les Strings.
and(X first, X second)
Est valide si les résultats des deux comparateurs utilisés en first et second sont vérifiés. Disponible pour tous les types primitifs et objets.
or(X first, X second)
Est valide si l'un des résultats des deux comparateurs utilisés en first et second est vérifié. Disponible pour tous les types primitifs et objets.
not(X value)
Est valide si le résultat du comparateur utilisé dans value est négatif.
cmpEq(X value)
Vérifie que la valeur reçue égale la valeur attendue du point de vue de Comparable.compareTo(X o). Disponible pour tous les types primitifs numériques et les implémentations de Comparable.
cmp(X value, Comparator<X> comparator, LogicalOperator operator)
Vérifie que comparator.compare(reçue, value) operator 0operator est <,<=,>,>= ou ==.
capture(Capture<T> capture), captureXXX(Capture<T> capture)
Laisse passer n'importe quelle valeur mais la capture dans le paramètre Capture pour un usage ultérieurs. Vous pouvez utiliser and(someMatcher(...), capture(c)) pour capturer le paramètre d'un appel de méthode en particulier. Vous pouvez aussi spécifier le CaptureType pour indiquer à l'objet Capture de conserver le premier (FIRST), le dernier (LAST), tous (ALL) ou aucun (NONE) des objets capturés

Définir son propre comparateur d'arguments

Il peut être intéressant de définir son propre comparateur d'argument. Prenons un comparateur dont le rôle serait de vérifier une exception par rapport à son type et message. Il pourrait être utilisé de la façon suivante:

    IllegalStateException e = new IllegalStateException("Operation not allowed.")
    expect(mock.logThrowable(eqException(e))).andReturn(true);

Deux étapes sont nécessaires pour réaliser cela: le nouveau comparateur doit être défini et la méthode statique eqException doit être déclarée.

Pour définir le nouveau comparateur d'argument, nous implémentons l'interface org.easymock.IArgumentMatcher. Cette interface contient deux méthodes: matches(Object actual), vérifiant que l'argument reçu est bien celui attendu, et appendTo(StringBuffer buffer), ajoutant au StringBuffer une chaîne de caractères représentative du comparateur d'argument. L'implémentation est la suivante :

import org.easymock.IArgumentMatcher;

public class ThrowableEquals implements IArgumentMatcher {
    private Throwable expected;

    public ThrowableEquals(Throwable expected) {
        this.expected = expected;
    }

    public boolean matches(Object actual) {
        if (!(actual instanceof Throwable)) {
            return false;
        }
        String actualMessage = ((Throwable) actual).getMessage();
        return expected.getClass().equals(actual.getClass())
                && expected.getMessage().equals(actualMessage);
    }

    public void appendTo(StringBuffer buffer) {
        buffer.append("eqException(");
        buffer.append(expected.getClass().getName());
        buffer.append(" with message \"");
        buffer.append(expected.getMessage());
        buffer.append("\"")");

    }
}

La méthode eqException doit instancier le comparateur d'argument avec l'objet Throwable donné, le fournir à EasyMock via la méthode statique reportMatcher(IArgumentMatcher matcher) et retourner une valeur afin d'être utilisée au sein de l'appel à la méthode mockée (typiquement 0, null ou false). Une première tentative ressemblerait à ceci:

public static Throwable eqException(Throwable in) {
    EasyMock.reportMatcher(new ThrowableEquals(in));
    return null;
}

Cependant, cela ne fonctionnerait que si la méthode logThrowable de l'exemple acceptait Throwables et quelque chose de plus spécifique du style de RuntimeException. Dans ce dernier cas, le code de notre exemple ne compilerait pas:

    IllegalStateException e = new IllegalStateException("Operation not allowed.")
    expect(mock.logThrowable(eqException(e))).andReturn(true);

Java 5.0 à la rescousse: Au lieu de définir eqException avec un Throwable en paramètre, nous utilisons un type générique qui hérite de Throwable:

public static <T extends Throwable> T eqException(T in) {
    reportMatcher(new ThrowableEquals(in));
    return null;
}

Réutilisation d'un Mock Object

Les Mock Objects peuvent être réinitialisés avec reset(mock).

Au besoin, un Mock Object peut aussi être converti d'un type à l'autre en appelant resetToNice(mock), resetToDefault(mock) ou resetToStrict(mock).

Utilisation d'un comportement de "stub" pour les méthodes

Dans certains cas, nous voudrions que nos Mock Object répondent à certains appels, mais sans tenir compte du nombre de fois, de l'ordre ni même s'ils ont été eu lieu. Ce comportement de "stub" peut être défini en utilisant les méthodes andStubReturn(Object value), andStubThrow(Throwable throwable), andStubAnswer(IAnswer<t> answer) et asStub(). Le code suivant configure le Mock Object pour répondre 42 à voteForRemoval("Document") une fois et -1 pour tous les autres arguments:

    expect(mock.voteForRemoval("Document")).andReturn(42);
    expect(mock.voteForRemoval(not(eq("Document")))).andStubReturn(-1);

Création de mocks dits "gentils"

Pour un Mock Object retourné par createMock(), le comportement par défaut pour toutes les méthodes est de lever une AssertionError pour tous les appels non prévus. Si vous souhaitez avoir un Mock Object "gentil" autorisant, par défaut, l'appel à toutes les méthodes et retournant la valeur vide appropriée (0, null ou false), utilisez createNiceMock() au lieu de createMock().

Méthodes de la classe Object

Les comportements des quatre méthodes equals(), hashCode(), toString() et finalize() ne peuvent être changés sur des Mock Objects créés avec EasyMock, même si elles font partie de l'interface duquel le Mock Object est créé.

Vérifier l'ordre d'appel des méthodes entre plusieurs Mocks

Jusqu'à présent, nous avons vu un Mock Object comme étant seul et configuré par les méthodes statiques de la classe EasyMock. Mais beaucoup de ces méthodes statiques font référence à l'objet "control" caché de chaque Mock Object et lui délègue l'appel. Un Mock Control est un objet implémentant l'interface IMocksControl.

Du coup, au lieu de

    IMyInterface mock = createStrictMock(IMyInterface.class);
    replay(mock);
    verify(mock); 
    reset(mock);

nous pourrions utiliser le code équivalent:

    IMocksControl ctrl = createStrictControl();
    IMyInterface mock = ctrl.createMock(IMyInterface.class);
    ctrl.replay();
    ctrl.verify(); 
    ctrl.reset();

L'interface IMocksControl permet de créer plus d'un seul Mock Object. Ainsi, il est possible de vérifier l'ordre d'appel des méthodes entre les mocks. Par exemple, configurons deux mock objects pour l'interface IMyInterface pour lesquels nous attendons respectivement les appels à mock1.a() et mock2.a(), un nombre indéfini d'appels à mock1.c() et mock2.c(), et enfin mock2.b() et mock1.b(), dans cet ordre:

    IMocksControl ctrl = createStrictControl();
    IMyInterface mock1 = ctrl.createMock(IMyInterface.class);
    IMyInterface mock2 = ctrl.createMock(IMyInterface.class);

    mock1.a();
    mock2.a();

    ctrl.checkOrder(false);

    mock1.c();
    expectLastCall().anyTimes();     
    mock2.c();
    expectLastCall().anyTimes();     

    ctrl.checkOrder(true);

    mock2.b();
    mock1.b();

    ctrl.replay();

Nommer un Mock Object

Les Mock Objects peuvent ê nommés à leur création en utilisant createMock(String name, Class<T> toMock), createStrictMock(String name, Class<T> toMock) ou createNiceMock(String name, Class<T> toMock). Les noms seront affichés dans le message des AssertionError.

Sérializer un Mock Object

Un Mock Object peut être sérializé à n'importe quelle étape de son existence. Il y a toutefois des contraintes évidentes:

Traitement multifil

Pendant la phase d'enregistrement un Mock Object n'est pas à fil sécurisé. Un Mock Object donné (ou des Mock Objects liés au même IMocksControl) ne peut être enregistré que d'un seul fil. Toutefois, plusieurs Mock Objects peuvent être enregistrés simultanément dans des fils différents.

Durant la phase de rejeu, un Mock Object sera à fil sécurisé par défaut. Ceci peut être changé en appelant makeThreadSafe(mock, false). durant la phase d'enregistrement. Cela peut permettre d'éviter des interblocages dans certaines rares situations.

Finallement, appeler checkIsUsedInOneThread(mock, true) permet de s'assurer qu'un Mock Object ne sera appelé que d'un seul fil. Une exception sera lancé sinon. Cela peut être pratique dans le cas où l'objet "mocké" n'est pas à fil sécurisé et que l'on veut s'assurer qu'il est utilisé correctement.

EasyMockSupport

EasyMockSupport est une classe ayant pour but d'être utilisée comme classe utilitaire ou comme classe de base de vos classes de test. Elle se souvient de tous les "Mock Objects" créés (ou en fait de tous les "Mock Controls" créés) pour pouvoir faire un replay, reset ou verify de tous en un seul coup. Voici un exemple utilisant JUnit:

public class SupportTest extends EasyMockSupport {

    private Collaborator firstCollaborator;
    private Collaborator secondCollaborator;
    private ClassTested classUnderTest;

    @Before
    public void setup() {
        classUnderTest = new ClassTested();
    }

    @Test
    public void addDocument() {
        // phase de création
        firstCollaborator = createMock(Collaborator.class);
        secondCollaborator = createMock(Collaborator.class);
        classUnderTest.addListener(firstCollaborator);
        classUnderTest.addListener(secondCollaborator);

        // phase d'enregistrement
        firstCollaborator.documentAdded("New Document");
        secondCollaborator.documentAdded("New Document");
        
        replayAll(); // tous les mocks d'un seul coup
        
        // test
        classUnderTest.addDocument("New Document", new byte[0]);
                
        verifyAll(); // tous les mocks d'un seul coup
    }
}

Modifier les comportements par défaut d'EasyMock

EasyMock fournit un mécanisme de gestion de propriétés permettant de modifier son comportement. Il vise principalement à permettre le retour à un comportement antérieur à la version courante. Les propriétés actuellement supportées sont:

easymock.notThreadSafeByDefault
Si true, les Mock Objects ne seront pas à fil sécurisé par défaut. Values possibles: "true" ou "false". Défaut: false
easymock.enableThreadSafetyCheckByDefault
Si true, un mock ne pourra être appelé que d'un seul fil. Values possibles: "true" ou "false". Défaut: false
easymock.disableClassMocking
Ne pas permettre le mocking de classes (permettre uniquement le mocking d'interfaces). Valeurs possibles: "true" ou "false". Défaut: false

Les propriétés peuvent être fixées de deux façons.

Compatibilité avec les anciennes versions

EasyMock 3 fournit toujours le project Class Extension (qui est toutefois déprécié) pour permettre une migration plus facile de EasyMock 2 vers EasyMock 3. Il s'agit d'une compatibilité des sources et non des binaires. Le code devra donc être recompilé.

EasyMock 2.1 introduisait une fonctionnalité de callback qui a été retirée dans EasyMock 2.2, car trop complexe. Depuis EasyMock 2.2, l'interface IAnswer fournit la fonctionnalité de callback.

OSGi

Le jar d'EasyMock peut être utilisé comme bundle OSGi. Il export les packages org.easymock, org.easymock.internal et org.easymock.internal.matchers. Toutefois, pour importer les deux derniers, vous spécifier l'attribut poweruser à "true" (poweruser=true). Ces packages sont prévus d'être utilisés pour étendre EasyMock, ils n'ont donc pas besoins d'être importés habituellement.

Mocking partiel

Dans certains cas, vous pouvez avoir besoin de "mocker" uniquement certaines méthodes d'une classe et de conserver un comportement normal pour les autres. Cela arrive habituellement lorsque pour souhaitez tester une méthode appelant d'autres méthodes de la même classe. Vous voulez donc garder le comportement normal de la méthode testée et "mocker" les autres.

Dans ce cas, la premier réflexe à avoir est d'envisager un refactoring car, bien souvent, ce problème est la conséquence d'un mauvais design. Si ce n'est pas le cas ou si vous ne pouvez faire autrement pour une quelconque contrainte de développement, voici la solution:

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

Seules les méthodes ajoutées avec addMockedMethod(s) seront "mockées" (mockedMethod() dans l'exemple). Les autres conservent leur comportement habituel. Une exception: les méthodes abstraites sont "mockées" par défaut.

createMockBuilder retourne l'interface IMockBuilder. Elle contient diverses méthodes pour facilement créer un mock partiel. Jettez un coup d'oeil à la javadoc pour en savoir plus.

Remarque: EasyMock fournit un comportement par défault pour les méthodes de la classe Object (equals, hashCode, toString, finalize). Toutefois, pour un mock partiel, si ces méthodes ne sont pas mockées explicitement, elles auront leur comportement normal et non celui par défaut d'EasyMock.

Test interne d'une classe

Il est possible de créer un mock en appelant un constructeur de la classe. Ceci peut être utile lorsqu'une méthode doit être testée mais d'autres dans la même classe "mockées". Pour cela vous devez faire quelque chose comme

ToMock mock = createMockBuilder(ToMock.class)
   .withConstructor(1, 2, 3); // 1, 2, 3 sont les paramètres passés au constructeur

Voir ConstructorCalledMockTest pour un exemple d'utilisation.

Remplacer l'instantiateur de classes par défaut

Parfois (habituellement à cause d'une JVM non supportée), il est possible que EasyMock ne soit pas capable de créer un mock dans votre environnement java. Sous le capot, l'instantiation de classes est implémentée par un pattern "factory". En cas de problème, vous pouvez remplacer l'instantiateur par défaut avec:

Vous assignez ce nouvel instantiateur à l'aide de ClassInstantiatorFactory.setInstantiator(). Vous pouvez remettre celui par défaut avec setDefaultInstantiator().

Important: L'instantiateur est gardé statiquement et reste donc entre deux tests. Assurez-vous de le réinitialiser si nécessaire.

Sérializer une classe mockée

Une class mockée peut aussi être sérializé. Toutefois, comme celle-ci étant une classe sérializable, cette dernière peut avoir un comportement spécial dû à l'implémentation de méthodes tels que writeObject. Ces méthodes seront toujours appelées lorsque le mock sera sérializé et peuvent potentiellement échouer. Habituellement, le contournement consiste à créer le mock en appelant un constructeur.

Aussi, il est possible que la dé-sérialization d'un mock ne fonctionne pas si elle est effectuée dans un class loader différent de la sérialization. Ce cas n'a pas été testé.

Limitations du mocking de classes

Pour être cohérent avec le mocking d'interfaces, EasyMock fournit aussi un comportement par défaut pour equals(), toString(), hashCode() et finalize() pour les classes mockées. Cela signifie que vous ne pourrez enregistrer votre propre comportement pour ces méthodes. Cette limitation être considérée comme une fonctionnalité permettant de ne pas s'occuper de ces méthodes.

Les méthodes finales ne peuvent pas être "mockées". Si appelées, leur code normal sera exécuté.

Les méthodes privées ne peuvent être "mockées". Si appelées, leur code normal sera exécuté. Pour un mock partiel, si la méthode testée appelle une méthode privée, vous devrez aussi tester cette dernière étant donné que vous ne pouvez pas la mocker.

L'instantiation des classes est faite par Objenesis. Les JVMs supportées sont listées ici.

Support Android

Depuis la version 3.2, EasyMock peut être utilisé sur une VM Android (Dalvik). Il suffit d'ajouter la dépendance à votre projet apk utilisé pour les tests de votre applicaiton. Il est aussi préférable d'exclure cglib étant donné que dexmaker sera de toute façon utiliser en remplacement. Vous devrez aussi ajouter dexmaker explicitement étant donné qu'il s'agit d'une dépendance optionnelle. Si vous utilisez Maven, vous finirez avec les dépendances suivantes

<dependency>
  <groupId>org.easymock</groupId>
  <artifactId>easymock</artifactId>
  <version>3.2</version>
  <exclusions>
    <exclusion>
      <groupId>cglib</groupId>
      <artifactId>cglib-nodep</artifactId>
    </exclusion>
  </exclusions>
</dependency>
<dependency>
  <groupId>com.google.dexmaker</groupId>
  <artifactId>dexmaker</artifactId>
  <version>1.0</version>
</dependency>

Développement d'EasyMock

EasyMock a été développé par Tammo Freese chez OFFIS. La maintenance est effectuée par Henri Tremblay depuis 2007. Le développement d'EasyMock est hébergé par SourceForge pour permettre à d'autres développeurs et sociétés d'y contribuer.

Les Mock Objects de classes (précédemment appelé EasyMock Class Extension) ont été initialement développée par Joel Shellman, Chad Woolley et Henri Tremblay dans la section fichiers du of Yahoo!Groups.

Remerciements à ceux qui nous ont fourni retour d'expérience et rustines, incluant Nascif Abousalh-Neto, Dave Astels, Francois Beausoleil, George Dinwiddie, Shane Duan, Wolfgang Frech, Steve Freeman, Oren Gross, John D. Heintz, Dale King, Brian Knorr, Dierk Koenig, Chris Kreussling, Robert Leftwich, Patrick Lightbody, Johannes Link, Rex Madden, David McIntosh, Karsten Menne, Bill Michell, Stephan Mikaty, Ivan Moore, Ilja Preuss, Justin Sampson, Markus Schmidlin, Richard Scott, Joel Shellman, Jiří Mareš, Alexandre de Pellegrin Shaun Smith, Marco Struck, Ralf Stuckert, Victor Szathmary, Bill Uetrecht, Frank Westphal, Chad Woolley, Bernd Worsch, Rodrigo Damazio, Bruno Fonseca, Ben Hutchison et de nombreux autres.

Merci de consulter la page d'accueil EasyMock pour être informé des nouvelles versions et transmettez vos bogues et suggestions à EasyMock Yahoo!Group (en anglais SVP). Si vous souhaitez souscrire au EasyMock Yahoo!Group, envoyez un message à easymock-subscribe@yahoogroups.com.