Utilisation de mocks (objets simulés)Nous allons présenter ici ce que sont les mock object permettant de simuler le comportement de certains objets afin de réaliser des tests.

Présentation

Les doublures d'objets ou les objets de type mock permettent de simuler le comportement d'autres objets. Ils peuvent trouver de nombreuses utilités notamment dans les tests unitaires où ils permettent de tester le code en maitrisant le comportement des dépendances.

Concrètement, nous utilisons des mock pour simuler le comportement d'autres classes. Plusieurs cas de figure peuvent justifier l'utilisation de mock :

  • Nous faisons appel à une resource qui n'est pas accessible hors production : le mock simulera alors la resource
  • Nous faisons appel à un traitement qui a déjà été testé et dont le temps d'exécution est long : dans la mesure où le traitement a déjà été testé, inutile de re-tester son fonctionnement, nous le simulons donc.
  • Nous voulons simuler un comportement difficilement reproductible : par exemple une erreur sur le réseau. Dans ce cas, nous ne pouvons que la simuler
  • Nous voulons utiliser un composant qui n'a pas encore été développé

Globalement, l'utilisation de mock peut être faite à chaque fois que nous faisons appel à une autre classe que celle que laquelle nous testons. Il convient cependant de ne pas multiplier les mock afin de ne pas complexifier le test : écrire un mock prend du temps.

Création par héritage

Cas général

La méthode la plus simple pour créer un mock est de créer une classe dans le dossier test qui hérite du service dont on souhaite simuler le comportement.

Si nous disposons de deux services ServiceAppele et ServiceAppelant tel qu'une méthode de ServiceAppelant fait appel à une méthode de ServiceAppele :

public class ServiceAppele {

    public void methodeTresLongue() {
        try {
            Thread.sleep(1000000);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ServiceAppelant {

    public void methodeATester(ServiceAppele service) {
        service.methodeTresLongue();
        // Suite du programme
    }
}

Pour créer un mock de service appelé, il suffit de créer une classe MockServiceAppele qui surcharge la méthode methodeTresLongue

public class MockServiceAppele extends ServiceAppele {

    public void methodeTresLongue() {
        System.out.println("Ne fait rien");
    }
}

Il suffiera alors de tester la classe ServiceAppelant en se basant sur le mock et non sur le service d'origine

public class ServiceAppelantTest {

    @Test
    public void tester() {
        new ServiceAppelant().methodeATester(new MockServiceAppele());
        Assert.assertEquals(true, true);
    }
}

Cette stratégie implique d'identifiant clairement les dépendances, ce n'est pas un souci si on développe un nouveau projet, il suffiera de toujours passer en paramètre les services utilisé. En revanche dans le cadre de la reprise d'une application en maintenance, il faudra se baser sur l'existant

Pattern Proxy

Dans le cadre de l'utilisation de mock par héritage, le design pattern proxy est très intéressant puisqu'il permettra de surcharger des composants dont on ne maitrise pas nécéssairement le code sans avoir à identifier les dépendances.

Un proxy sur la connexion à une base de données est presque toujours une bonne idée et permettra facimement de basculer sur une base ou un mécanisme de test alternatif

Utilisation de Mockito

Introduction

Mockito est un framework Java, permettant de mocker ou espionner des objets, simuler et vérifier des comportements, ou encore simplifier l’écriture de tests unitaires.

Mise en place

Pour utiliser mockito, il suffiera de placer les lignes suivantes dans le fichier pom.xml

<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.5.13</version>
    <scope>test</scope>
</dependency>

Ecriture du mock

Pour mocker une classe, il suffit d'appeler Mockito.mock() en indiquant quelle classe on souhaite mocker.

Il est possible de définir les résultats des méthodes de la classe simulée en appelant Mockito.when().thenReturn()

public class ClasseTest {

	@Test
	public void test1(){
		User user = Mockito.mock(User.class);
		System.out.println(user.getLogin()); // affiche null
		user.setLogin("bob");
		System.out.println(user.getLogin()); // affiche encore null !
		Mockito.when(user.getLogin()).thenReturn("bob");
		System.out.println(user.getLogin()); // affiche "bob"
	}
 
}

Vérifications avec Mockito

L'idée est de dire : "Mockito, vérifie pour moi que la méthode m1() a été appelée une fois, avec exactement les arguments x et y".

Ou encore "Mockito, vérifie pour moi que la méthode m2() n’a jamais été appelée avec un argument de type Long et dont la valeur est inférieure à 10".

// vérifie que la méthode m1 a été appelée sur obj, avec une String strictement égale à "bob"
Mockito.verify(obj).m1(Mockito.eq("bob"));
// note : ici, le matcher n'est pas indispensable, la ligne suivante est équivalente :
Mockito.verify(obj).m1("bob");
 
// vérifie que la méthode m1 a été appelée sur obj, avec un objet similaire à celui passé en argument
Mockito.verify(obj).m1(Mockito.refEq(obj2));
 
// vérifie que la méthode m2 n'a jamais été appelée sur l'objet obj
Mockito.verify(obj, Mockito.never()).m2();
 
// vérifie que la méthode m3 a été appelée exactement 2 fois sur l'objet obj
Mockito.verify(obj, Mockito.times(2)).m3();
 
// idem avec un nombre minimum et maximum
Mockito.verify(obj, Mockito.atLeast(3)).m3();
Mockito.verify(obj, Mockito.atMost(10)).m3();
Mise en garde

L'utilisation de mockito présente de nombreux intérets :

  • Les mocks sont réalisés plus rapidement que lors d'un héritage
  • Il est facile d'adapter le comportement d'une méthode et de modifier ses sorties en cours de test
  • Les tests sont plus lisibles, nous n'avons pas besoin de créer des sous-classes

En revanche, l'utilisation de mock ne doit pas devenir un réflèxe dès lors qu'on utilise une classe autre que la classe testée, en effet, l'écriture de mocks évolués peut prendre de temps et présente un inconvénient de taille : ce n'est plus notre code qui est testé mais le mock. Lors d'une maintenance du code, le mock doit également être maintenu ce qui est difficile à faire parce qu'un mock et sa classe mère ne sont pas réellement attachés et que les mocks sont disséminés dans le code

L'utilisation des vérifications avec Mockito menace la maintenabilité de nos tests unitaires : un tout petit refactoring qui ne change rien fonctionnelement peut faire échouer un test parce que d'autres méthodes ont été appelées. Ce qu'on teste c'est un résultat et non le chemin pris pour l'obtenir.

Mockito doit donc être utilisé avec prudence, il est très efficace pour simuler le comportement de resources non disponnibles, il peut être intéressant de ne pas pousser plus loin son utilisation