Participer au site avec un Tip
Rechercher
 

Améliorations / Corrections

Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.

Emplacement :

Description des améliorations :

Utilisation de « Mock objects » dans vos tests

Code Coverage Pourquoi utiliser une API de logging ?



Accès rapide :
Comment bien architecturer son programme pour faciliter les tests.
Introduction très rapide au TDD
Modéliser vos composants logiciels
Couplez vos composants par interfaces
Et les tests dans tout ça ?
Qu'est qu'un « Mock Object » (un bouchon en français) ?
Quelques frameworks de bouchonnage
Le framework JMock
Le framework EasyMock
Le framework Mockito
Un cas concret d'utilisation de « mock objects »
Présentation de notre composant de sécurité et de ses interfaces
Bouchonnage du composant de sécurité dans nos tests unitaires
Travaux pratiques
Le sujet
La correction
Pour aller plus loin

Comment bien architecturer son programme pour faciliter les tests.

Introduction très rapide au TDD

Il existe une méthodologie de développement d'application orientée test : son nom, « Test Driven Development » (TDD pour les intimes). Sans prétendre vous expliquer TDD en détails dans ce chapitre, je veux juste porter mon attention sur un aspect fondamental de la méthodologie : TDD préconise d'écrire les tests unitaires avant de commencer à coder le composant logiciel associé.

A ce stade, deux questions vous brûlent peut-être les lèvres :

Modéliser vos composants logiciels

Quand vous allez commencer à travailler sur des gros projets Java, il va être nécessaire de commencer à réfléchir avant de passer au code. Ca peut paraître évident, mais je vois encore trop souvent des projets débuter directement par le code. Et dans ce cas, j'émets de gros doute sur la réussite du projet et sur la pertinence de la procédure de tests unitaires.

Encore une fois, je ne vais pas prétendre ici faire un cours sur l'architecture logicielle (ça prendrait trop de temps), mais je voudrais en rappeler quelques fondamentaux.

Une des étapes clé de votre processus de modélisation de l'application à développer consiste à découper l'application en composants logiciels. D'une granularité inférieure, il sera plus aisé de travailler composant par composant. Il est aussi important de noter qu'un composant peu plus facilement être réutilisé sur un autre projet.

Un composant logiciel peut être vu comme un élément constitutif du logiciel à développé qui agrège un ensemble de fonctionnalités cohérentes entre elles. Pour représenter vos composants logiciels, vous pouvez utiliser le formalisme graphique UML (Unified Modeling Language). Des modeleurs UML existent afin de vous simplifier la réalisation de vos diagrammes.

pour de plus amples informations sur UML, vous pouvez consulter notre support de cours sur le sujet.
si, comme moi, vous utiliser l'atelier de développement Eclipse, vous pouvez y ajouter le modeleur UML Papyrus. Il s'agit d'un projet officiel de la fondation Eclipse.

Un composant se représente en UML via l'un des trois pictogrammes suivant : le visuel dépend du modeleur UML utilisé.

Pictogrammes UML de représentation de composants logiciels.
le dernier pictogramme utilise la notion de stéréotype UML. Un stéréotype est à UML ce qu'une annotation est à Java : dit-autrement, une méta-donnée.

Imaginons que nous souhaitions développer un site de vente en ligne. Nous pourrions alors considérer le diagramme UML de composants suivant. Clairement, j'ai simplifié les choses, car un véritable site de vente en ligne pouvant être bien plus complexe.

Diagramme UML de composants

On trouve donc, dans ce diagramme, quatre composants logiciels. Voici à quoi correspond chacun d'eux :

Pour chaque composant, nous allons devoir écrire un jeu de tests unitaires et chaque composant devra être testé indépendamment des autres composants. Mais comment se passer des autres composants dans la batterie de tests unitaires d'un composant ?

Couplez vos composants par interfaces

Ce point est certainement le plus important de tous le chapitre. Dans le diagramme UML précédent, les ronds représentent les interfaces de chaque composant. Le trait plein entre un composant est son interface représente la notion d'implémentation d'interface.

une interface associée à un composant logiciel est aussi couramment appelée une API (Application Programming Interface).

Mais, pourquoi est-ce si important d'avoir ces interfaces ?

Une première réponse pourrait être que grâce à cette architecture, on peut aussi imaginer répartir chaque composant sur une équipe de développement spécifique. Du coup, quatre équipes différentes peuvent travailler en parallèle sur le même projet. Comme les interfaces de communication inter-composants sont normalement modélisées en UML, il est possible d'entre produire les codes via les outils de génération de code souvent présents dans les modeleurs UML. Ainsi les interfaces deviennent de données d'entrées du processus de développement, garantissant que tout le monde travaille bien sur « le même contrat ». Les composants pourront ainsi être assemblés par la suite avec la garantie qu'ils vont pouvoir communiquer.

Une seconde réponse réside, vous vous en doutez, dans notre besoin de tester chaque composant unitairement.

Et les tests dans tout ça ?

Eh bien, c'est à partir de là que tout devient évident : si chaque composant est bien couplé aux autres par interface, il est donc possible d'écrire des scénarios de tests unitaires composant par composant. Dans le cadre d'un test, on appelle SUT (System Under Test) le composant en test. Pour chaque dépendance, au lieu de passer au SUT un pointeur vers un autre composant réel, on va pouvoir lui passer une implémentation « bidon » de l'interface simulant le comportement attendu. Il est donc possible de tester un composant indépendamment des autres grâce à une fausse implémentation (un simulateur) pour chaque dépendance.

Grace à une injection de dépendance, on pourra passer au SUT l'implémentation de simulation en lieu et place de l'implémentation réelle.

l'injection de dépendance est un concept qui fait souvent peur. Pour autant les choses sont très simples, souvent l'injection se fait grâce à un setter qui accepte en paramètre une interface banale. Ainsi, par polymorphisme, il sera possible d'injecter dans le composant une implémentation ou une autre (un composant réel ou un simulateur).

CQFD : la boucle est bouclée. Si l'on introduit chaque composant par son interface (son API), il devient possible d'écrire nos tests avant même d'avoir les code des composants. Je vous rappelle qu'on peut produire les codes Java de nos interfaces grâce à notre outil de modélisation UML. Donc, si nous avons fait l'effort de bien modéliser l'application avant de coder, on peut générer les interfaces et donc commencer par écrire les tests qui permettront de valider les codes restant à produire (à condition de typer par interface, bien entendu). L'axiome principal de l'approche TDD est donc tout à fait cohérent et réalisable.

En conclusion, je dirais qu'une bonne stratégie de tests présuppose un travail d'architecture du logiciel en amont. Sans ce travail d'architecture, je ne suis pas sûr que vous aurez les interfaces de communication inter-composants et donc « bye bye » les procédures de tests unitaires sérieuses.

je vous propose de bien méditer ces points avant de poursuivre ce cours !

Qu'est qu'un « Mock Object » (un bouchon en français) ?

En fait, on a déjà tout dit ! Un « mock object » est un simulacre qui se fait passer pour autre chose (ce que j'ai appelé précédemment un simulateur). Normalement, un « mock object » remplace un composant logiciel et implémente la même interface que ce dernier : c'est ce qui permet de faire croire au SUT qu'il est dans une vraie intégration de composants, mais en vérité, il tourne dans un environnement simulé.

Le terme de « Mock Object » vient que mot anglais « mockery » qui se prononce comme sa traduction française (moquerie). On fait donc une blague à un composant en lui faisant croire qu'il s'exécute dans la vraie vie, mais il en est rien. Certains d'entre vous pourront y voir une comparaison avec « la matrice de Neo » ;-)

Le terme équivalent francophone est un peu différents de celui utilisé par les anglophones : on parle de bouchon. Si l'on considère que le SUT est une bouteille mis en plat, qu'est-ce qui sort de la bouteille ? Nous diront que c'est les « outputs ». Si l'on écrit un test unitaire, il faudra récupérer les sorties (les outputs) afin de vérifier que les données produites sont bien conformes aux attendus. Mais qu'est-ce qui récupère la sortie de la bouteille ? Eh bien, un bouchon ! Peut-être que cette vision était inéluctable dans le pays du vin ;-)

personnellement, j'ai une préférence pour la terminologie et le sens anglophone, d'où le fait que je parlerai plus souvent de « Mock Object ».

Quelques frameworks de bouchonnage

La nouvelle question est maintenant de savoir comment coder nos « Mock Objects » (nos bouchons) ? Deux approches sont possibles.

C'est, bien entendu, la seconde stratégie que je vais vous montrer. Et il existe plusieurs frameworks de ce type. Bien qu'il y en ait beaucoup, j'en ai retenu trois : JMock, EasyMock et Mockito. Nous allons, avec ces trois solutions tenter de générer un mock pour l'interface suivante.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
package fr.koor.mock;

public interface Subscriber {

    public boolean init();
    
    public void receive( String theMessage );
    
    public boolean destroy();
    
    // Plein de méthodes qui ne servent à rien dans notre scénario de test,
    // mais qui doivent être implémentées dans notre mock.
    public void demoX();
    public void demoY();
    public void demoZ();
    
}
Fichier Subscriber.java : l'interface pour laquelle produire un mock

Quel que soit le framework de bouchonnage utilisé, c'est le même mécanisme qui est utilisé pour produire le mock : la réflexion Java. Grace à la réflexion, le framework va pouvoir trouver les méthodes exposées par l'interface à bouchonner. Il va ensuite créer une classe implémentant cette interface et donc, chacune de ses méthodes.

Le code à tester sera le suivant : il s'agit d'une classe initiant des appels sur des objets basés sur l'interface précédente. Il est important de noter que cette classe est liée aux « subscribers » par interface ;-)

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
package fr.koor.mock;

import java.util.ArrayList;

public class Publisher {

    private ArrayList<Subscriber> subscribers = new ArrayList<Subscriber>(); 
    
    
    public void addSubscriber( Subscriber subsc ) {
        this.subscribers.add( subsc );
    }
    
    
    public void init() {
        for( Subscriber subscriber : this.subscribers ) {
            if ( subscriber.init() == false ) {
                throw new RuntimeException();
            }
        }
    }
    
    public void publish( String message ) {
        for( Subscriber subscriber : this.subscribers ) {
            subscriber.receive( message.toUpperCase() );
        }       
    }

    public void destroy() {
        for( Subscriber subscriber : this.subscribers ) {
            if ( subscriber.destroy() == false ) {
                throw new RuntimeException();
            }
        }
        this.subscribers.clear();
    }
    
}
Fichier Publisher.java : le code à tester
oui, je sais que je vous propose le code à tester avant de parler du test, mais pédagogiquement parlant, les choses sont plus simples. Comprenez que pour le moment nous cherchons à apprendre à manipuler un framework de bouchonnage. Quand vous serez plus aguerris sur ces techniques de développement, vous pourrez appliquer l'axiome de base de TDD.

Le framework JMock

JMock est l'un des frameworks de bouchonnage historique de Java. Il est disponible à l'adresse http://jmock.org/.

Pour utiliser JMock, il vous faut l'installer, soit en le téléchargeant directement à partir du site Web officiel (http://jmock.org/), soit en utilisant Maven.

Maven est un outil de build permettant de construire un programme Java et en gérant de manière automatique les dépendances (les librairies) utilisées par votre projet. Si vous ne connaissez pas encore Maven, sachez que nous en parlerons dans un futur chapitre de ce tutoriel. Je vous recommande l'utilisation de Maven pour la gestion des dépendances. Voici, à titre d'information, la configuration à rajouter dans le fichier pom.xml (le fichier de configuration Maven de votre projet) pour télécharger JMock.
 1 
 2 
 3 
 4 
 5 
 6 
 7 
<!-- https://mvnrepository.com/artifact/org.jmock/jmock -->
<dependency>
    <groupId>org.jmock</groupId>
    <artifactId>jmock</artifactId>
    <version>2.12.0</version>
    <scope>test</scope>
</dependency>
Ajout de la dépendance JMock dans le fichier pom.xml (vérifier quelle est la version la plus récente).

Pour produire un mock, il faut instancier une Mockery JMock. Ensuite, il faut y déclarer quelles sont nos attentes (expectations, en anglais). Une API dédiée à la définition des attentes vous est proposée. Voici un exemple de définition de mock : quelques explications suivront.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
package fr.koor.mock;

import java.lang.reflect.Field;
import java.util.ArrayList;

import org.jmock.Expectations;
import org.jmock.Mockery;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;


public class PublisherTest {

    private static final String sendedMessage = "Hello World";
    private static final String receivedMessage = "HELLO WORLD";
    
    private Mockery mockContext1 = null;
    private Mockery mockContext2 = null;
    
    @BeforeEach 
    public void setUp() {
        mockContext1 = new Mockery();
        mockContext2 = new Mockery();
    }
    
    @Test 
    public void testScenarioSuccess() throws Exception {
        Publisher publisher = new Publisher();        

        // Partie 1 : création de deux mocks.
        final Subscriber subscriber1 = mockContext1.mock( Subscriber.class );
        publisher.addSubscriber( subscriber1 );

        final Subscriber subscriber2 = mockContext2.mock( Subscriber.class );
        publisher.addSubscriber( subscriber2 );

        // Partie 2 : nos attentes sont injectées dans les mocks.
        mockContext1.checking( new Expectations() {{
            // Un appel à la méthode init est requis et elle doit retourner true. 
            oneOf( subscriber1 ).init();        will( returnValue( true ) );
            // Un appel à la méthode receive est requis avec la valeur spécifiée
            // en paramètre ("HELLO WORLD").
            oneOf( subscriber1 ).receive( receivedMessage );
            // Un appel à la méthode destroy est requis et elle doit retourner true. 
            oneOf( subscriber1 ).destroy();     will( returnValue( true ) );
        }});
        mockContext2.checking( new Expectations() {{
            oneOf( subscriber2 ).init();        will( returnValue( true ) );
            oneOf( subscriber2 ).receive( receivedMessage );
            oneOf( subscriber2 ).destroy();     will( returnValue( true ) );
        }});
        
        // Partie 3 : On lance le test.
        publisher.init();
        publisher.publish( sendedMessage );
        publisher.destroy();
        
        // Partie 4 : On vérifie les attentes.
        mockContext1.assertIsSatisfied();
        mockContext2.assertIsSatisfied();
    }
    
    
    @Test
    public void testScenarioFailure() throws Exception {
        Assertions.assertThrows( RuntimeException.class, () -> {
            Publisher publisher = new Publisher();        

            // Partie 1 : création d'un mock.
            final Subscriber subscriber1 = mockContext1.mock( Subscriber.class );
            publisher.addSubscriber( subscriber1 );
    
            // Partie 2 : nos attentes sont injectées dans le mock.
            mockContext1.checking( new Expectations() {{
                oneOf( subscriber1 ).init();        will( returnValue( false ) );
                oneOf( subscriber1 ).receive( receivedMessage );
                oneOf( subscriber1 ).destroy();     will( returnValue( true ) );
            }});
            
            // Partie 3 : on lance le test.
            publisher.init();
            publisher.publish( sendedMessage );
            publisher.destroy();
            
            // Partie 4 : on vérifie les attentes.
            mockContext1.assertIsSatisfied();
        });
    }

}
Exemple de définition d'un bouchon via JMock

Comme vous le constatez, deux méthodes de tests sont proposées dans cet exemple et c'est le framework JUnit 5.0 qui est utilisé. La première méthode teste un cas d'utilisation en succès alors que la seconde teste un cas d'erreur. Les moqueries, nécessaires aux deux méthodes de tests sont instanciées dans la méthode setUp.

L'obtention du mock à proprement parler, s'obtient via la ligne de code suivante : il s'agit d'une méthode générique. Le type de retour de la méthode est donc l'interface initialement passée en paramètre.

 1 
Subscriber subscriber1 = mockContext1.mock( Subscriber.class );
Obtention d'un mock

Pour la définition des attentes, une classe anonyme basée sur le type Expectations est utilisée. La deuxième paires d'accolades correspond à la définition du constructeur de la classe anonyme. C'est la manière recommandée pour définir nos attentes avec JMock.

comme notre test n'utilise pas les méthodes demoX, demoY et demoZ de l'interface, rien n'est dit sur ces méthodes. Elles seront générées à vide.

Le framework EasyMock

EasyMock est un autre framework de bouchonnage qui existe, lui aussi, depuis longtemps (début des années 2000). Il peut être téléchargé à partir de l'adresse suivante : https://easymock.org/.

comme dans l'exemple précédent, sachez qu'il est aussi possible de télécharger EasyMock via Maven. Si vous ne connaissez pas encore Maven, sachez que nous en parlerons dans un futur chapitre de ce tutoriel. Voici la configuration à rajouter dans le fichier pom.xml (le fichier de configuration Maven de votre projet) pour télécharger EasyMock.
 1 
 2 
 3 
 4 
 5 
 6 
 7 
<!-- https://mvnrepository.com/artifact/org.easymock/easymock -->
<dependency>
    <groupId>org.easymock</groupId>
    <artifactId>easymock</artifactId>
    <version>4.2</version>
    <scope>test</scope>
</dependency>
Ajout de la dépendance EasyMock dans le fichier pom.xml (vérifier quelle est la version la plus récente).

Comme vous allez le constater, EasyMock ressemble beaucoup à JMock sur la manière de déclarer un bouchon. Voici un exemple équivalent au précédent, mais qui utilise EasyMock.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
package fr.koor.mock;

import org.easymock.EasyMock;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;


public class PublisherTest {

    private static final String sendedMessage = "Hello World";
    private static final String receivedMessage = "HELLO WORLD";
    
    @Test 
    public void testScenarioSuccess() throws Exception {
        Publisher publisher = new Publisher();        
    
        // Partie 1 : création de deux mocks.
        final Subscriber subscriber1 = EasyMock.createMock( Subscriber.class );
        publisher.addSubscriber( subscriber1 );

        final Subscriber subscriber2 = EasyMock.createMock( Subscriber.class );
        publisher.addSubscriber( subscriber2 );

        // Partie 2 : nos attentes sont injectées dans les mocks.
        // On attend un appel à init avec une valeur de retour true.
        EasyMock.expect( subscriber1.init() ).andReturn( true );
        // On attend un appel à la méthode receive (qui renvoie void).
        subscriber1.receive( receivedMessage );
        EasyMock.expectLastCall();
        // On attend un appel à destroy avec une valeur de retour true.
        EasyMock.expect( subscriber1.destroy() ).andReturn( true );
        // On a terminer la défintion des attentes du mock subscriber1.
        EasyMock.replay( subscriber1 );

        EasyMock.expect( subscriber2.init() ).andReturn( true );
        subscriber2.receive( receivedMessage );
        EasyMock.expectLastCall();
        EasyMock.expect( subscriber2.destroy() ).andReturn( true );
        EasyMock.replay( subscriber2 );

        // Partie 3 : On lance le test.
        publisher.init();
        publisher.publish( sendedMessage );
        publisher.destroy();
        
        // Partie 4 : On vérifie les attentes.
        EasyMock.verify( subscriber1 );
        EasyMock.verify( subscriber2 );
    }
    
    
    @Test( expected = RuntimeException.class )
    public void testScenarioFailure() throws Exception {
        Assertions.assertThrows( RuntimeException.class, () -> {            
            Publisher publisher = new Publisher();        

            // Partie 1 : création de deux mocks.
            final Subscriber subscriber1 = EasyMock.createMock( Subscriber.class );
            publisher.addSubscriber( subscriber1 );
    
            // Partie 2 : nos attentes sont injectées dans le mock.
            EasyMock.expect( subscriber1.init() ).andReturn( false );
            subscriber1.receive( receivedMessage );
            EasyMock.expectLastCall();
            EasyMock.expect( subscriber1.destroy() ).andReturn( true );
            EasyMock.replay( subscriber1 );
            
            // Partie 3 : On lance le test.
            publisher.init();
            publisher.publish( sendedMessage );
            publisher.destroy();
            
            // Partie 4 : On vérifie les attentes.
            EasyMock.verify( subscriber1 );
        });
    }

}
Exemple de définition d'un bouchon via EasyMock

Ici, plus de Mockery : on peut juger que c'est plus simple. L'acquisition du mock est assez similaire au framework précédent. Ce qui change, c'est la syntaxe utilisée pour définir les attentes (expectations). Là, il n'y a pas de discussion possible : l'absence de la classe anonyme allège considérablement la syntaxe. Il faut quand même noter qu'on doit activer une fois le mock pour lui indiquer ce qu'on attend, puis lancer la méthode EasyMock.replay sur le mock pour le mettre en « mode enregistrement ». Du coup le mock va collecter la seconde salve d'appels de méthodes et il ne restera plus qu'à vérifier que tout à bien fonctionné en appelant la méthode EasyMock.verify.

Le framework Mockito

Le dernier framework de test que je voulais vous présenter est Mockito. Son nom est un mix entre Mock et Mojito (la célèbre boisson), d'où son logo remarquable. Il est accessible à partir de l'adresse https://site.mockito.org/.

encore une fois, sachez qu'il est aussi possible de télécharger Mockito via Maven. Et c'est d'autant plus recommandé que Mockito a lui-même des dépendances (d'autres librairies) utiles à son fonctionnement. Si vous ne connaissez pas encore Maven, sachez que nous en parlerons dans un futur chapitre de ce tutoriel. Voici la configuration à rajouter dans le fichier pom.xml (le fichier de configuration Maven de votre projet) pour télécharger Mockito.
 1 
 2 
 3 
 4 
 5 
 6 
 7 
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.4.4</version>
    <scope>test</scope>
</dependency>
Ajout de la dépendance Mockito dans le fichier pom.xml (vérifier quelle est la version la plus récente).

A la base, il s'agissait d'une extension du framework EasyMock, mais qui depuis a bien évoluée et notamment avec la généralisation de l'utilisation d'annotations. Voici un exemple de bouchon équivalent aux exemples précédents.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
package fr.koor.mock;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;


public class PublisherTest {

    private static final String sendedMessage = "Hello World";
    private static final String receivedMessage = "HELLO WORLD";
    
    @Mock
    private Subscriber subscriber1;

    @Mock
    private Subscriber subscriber2;
    
    @BeforeEach
    public void setUp() {
        MockitoAnnotations.openMocks(this);
    }
    
    @Test 
    public void testScenarioSuccess() throws Exception {
        
        // Partie 1 : création de deux mocks.
        Publisher publisher = new Publisher();
        publisher.addSubscriber( subscriber1 );
        publisher.addSubscriber( subscriber2 );

        // Partie 2 : on injecte nos implémentations dans le mock
        Mockito.when( subscriber1.init() ).thenReturn( true );
        Mockito.when( subscriber1.destroy() ).thenReturn( true );

        Mockito.when( subscriber2.init() ).thenReturn( true );
        Mockito.when( subscriber2.destroy() ).thenReturn( true );

        // Partie 3 : On lance le test.
        publisher.init();
        publisher.publish( sendedMessage );
        publisher.destroy();
        
        // Partie 4 : On vérifie les attentes.
        Mockito.verify( subscriber1 ).init();
        Mockito.verify( subscriber1 ).receive( receivedMessage );
        Mockito.verify( subscriber1 ).destroy();
        
        Mockito.verify( subscriber1, Mockito.times( 1 ) ).init();
        Mockito.verify( subscriber2 ).receive( receivedMessage );
        Mockito.verify( subscriber1, Mockito.times( 1 ) ).destroy();
    }

}
Exemple de définition d'un bouchon via Mockito

Une grosse différence, par rapport aux exemples précédents, réside dans l'utilisation des annotations. Attention, il ne faut pas oublier de démarrer le moteur Mockito pour que les injections de mocks soient réalisées (MockitoAnnotations.openMocks(this);).

si vous suivez le tutoriel officiel de Mockito, vous noterez que ses auteurs préfèrent l'utilisation de import static org.mockito.Mockito.*;. Dans ce cas, vous n'êtes plus obligé de spécifier les préfixes Mockito..

Bien qu'ayant beaucoup travaillé avec le framework JMock et étant fan des annotations, ma préférence va, aujourd'hui, plus à Mockito. C'est lui que je vais utiliser dans l'exemple suivant.

Un cas concret d'utilisation de « mock objects »

Nous allons de nouveau considérer notre exemple de site de vente en ligne. Nous partons de l'idée que nous souhaitons réaliser un test sur une partie de code du composant CommandComponent : or, celui-ci définit une dépendance sur le composant SecurityComponent. Il est donc nécessaire de bien connaître l'interface associée au composant SecurityComponent, car nous allons devoir la « bouchonner ».

le package définissant les interfaces du composant de sécurité peuvent être téléchargé sur GitHub (avec le reste du code du composant de sécurité). Voici l'URL du projet SecurityComponent : https://github.com/koor-fr/SecurityModule. Vous pourrez soit y télécharger un ZIP ou bien cloner le projet GIT (pour ceux qui savent déjà faire). Nous parlerons aussi de GIT dans un futur chapitre.

Présentation de notre composant de sécurité et de ses interfaces

une interface (de haut niveau d'abstraction : une API) définie dans un diagramme de composants peut se transformer en une ou plusieurs interfaces techniques Java. C'est ce cas que nous allons considérer avec notre composant de sécurité. Effectivement, définir un moteur de sécurité n'est pas si simple que cela. Un autre exemple serait l'interface JDBC d'accès aux bases de données : si vous ouvrez la documentation du package JDBC, vous observerez qu'il contient, lui aussi, plusieurs interfaces Java.

Le diagramme de classes UML qui suit montre le contenu des cinq interfaces Java associées à notre composant de sécurité. Quelques explications suivront.

Les interfaces de l'API SecurityComponent.

Quelques autres classes d'exceptions (pour la gestion des erreurs) font aussi partie de l'API de notre composant de sécurité. Pour plus de détails sur ces classes d'exceptions, je vous renvoie vers le projet GIT.

Maintenant que ces interfaces ont été présentées, nous pouvons passer au bouchonnage du moteur de sécurité.

Bouchonnage du composant de sécurité dans nos tests unitaires

Nous allons maintenant considérer le code suivant : c'est ce que nous allons devoir tester. La question étant de savoir comment nous allons bouchonner nos interfaces relatives au moteur de sécurité pour tester ce code ? La réponse est simple : nous allons utiliser un framework de bouchonnage et plus précisément Mockito.

Ce premier extrait de code correspond au code à tester. Vous noterez qu'il est censé utiliser un SecurityManager qui sera injecté dans le composant grâce à la méthode setSecurityManager. Ensuite la méthode doSomething porte le code à tester. A ce niveau il vous faudra faire un peu preuve d'imagination, car je n'y ai pas mis grand-chose. Le point le plus important à constater, c'est que cette méthode utilise le SecurityManager porté par notre classe : on doit donc avoir une instanciation d'objet compatible avec l'interface attendue. C'est donc bien là que nos mocks vont devoir faire « le job »

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
package fr.koor.command;
            
import fr.koor.security.Role;
import fr.koor.security.SecurityManager;
import fr.koor.security.SecurityManagerException;
import fr.koor.security.User;

public class ClassToValidate {

    private SecurityManager securityManager;
    
    // Un setter pour injecter un securityManager.
    public void setSecurityManager( SecurityManager securityManager ) {
        
        if ( securityManager == null ) {
            throw new NullPointerException( "securityManager cannot be null" );
        } 
        this.securityManager = securityManager;
        
    }

    // La méthode à tester.
    public boolean doSomething( User user ) throws SecurityManagerException {
        
        Role role = securityManager.getRoleManager().selectRoleByName( "admin" );
        if ( ! user.isMemberOfRole( role ) ) {
            throw new SecurityManagerException( "Only admin is autorized" );
        }
        
        // Imaginez ici du code plus complexe.
        
        return true;
        
    }

}
Fichier fr/koor/command/ClassToValidate.java : le code à tester

Et voici maintenant un exemple de test unitaire déclenchant la méthode doSomething ainsi que la mise en oeuvre des mocks permettant de bouchonner notre composant de sécurité. J'utilise le moteur d'annotations de Mockito pour réaliser, à ma place, les instanciations des mocks. Il me reste qu'à injecter les mocks les uns dans les autres pour simuler un moteur de sécurité (voir la méthode setUp).

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
package fr.koor.mock;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import fr.koor.command.ClassToValidate;
import fr.koor.security.Role;
import fr.koor.security.RoleManager;
import fr.koor.security.SecurityManager;
import fr.koor.security.SecurityManagerException;
import fr.koor.security.User;
import fr.koor.security.UserManager;


public class ClassToValidateTest {

    // On déclare nos cinq mocks pour nos 5 interfaces.
    @Mock
    private User user;
    
    @Mock
    private Role role;
    
    @Mock
    private UserManager userManager;
    
    @Mock 
    private RoleManager roleManager;
    
    @Mock
    private SecurityManager securityManager;
    
    @BeforeEach
    public void setUp() throws SecurityManagerException {
        // On instancie et on injecte les mocks.
        MockitoAnnotations.openMocks(this);
        
        // Et on les assemble les uns au autres.
        Mockito.when( roleManager.selectRoleByName( "admin" ) ).thenReturn( role );
        Mockito.when( user.isMemberOfRole( role ) ).thenReturn( true );
        
        Mockito.when( securityManager.getUserManager() ).thenReturn( userManager );
        Mockito.when( securityManager.getRoleManager() ).thenReturn( roleManager );
    }
    
    @Test 
    public void testScenarioSuccess() throws Exception {
        // On lance notre test.
        ClassToValidate codeToValidate = new ClassToValidate();
        codeToValidate.setSecurityManager( securityManager );
        
        boolean result = codeToValidate.doSomething( user );
        
        // On vérifie que tout a fonctionné correctement. 
        Assertions.assertTrue( result );
        Mockito.verify( securityManager, Mockito.times( 1 ) ).getRoleManager();
    }

}
Fichier fr/koor/mock/ClassToValidateTest.java : le test JUnit / Mockito
il est important de prendre la mesure de la facilité avec laquelle nous avons simulé nos 5 interfaces. Avec un bouchon implémenté manuellement, vous auriez dû fournir un code, même si le test ne les utilise pas, pour chaque méthode des cinq interfaces (quitte à ce que presque toutes ces implémentation de méthodes soient vides). Si vous avez du mal à vous rendre compte de la difficulté, je vous propose de coder vos mocks manuellement : c'est un très bon exercice, et au terme de cette phase de codage, je suis certains que vous apprécierez fortement l'utilisation d'un framework de bouchonnage.

Travaux pratiques

Le sujet

Nous allons maintenant considérer un nouveau composant : celui de gestion du stock de notre site de vente en ligne. Il s'agit encore une fois d'une caricature, mais j'ai néanmoins spécifié deux interfaces Java dans l'API (Application Programming Interface) de ce composant.

La première interface permet de représenter un article au sein du stock. En voici sa définition.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
package fr.koor.stock;

/**
 * This interface represent an article in the StockManager component.
 */
public interface StockArticle {

    /**
     * This method returns the primary key of the article.
     * @return the article's primary key.
     */
    int getIdArticle();
    
    /**
     * This method returns the description of the article.
     * @return the article's description.
     */
    String getDescription();

    /**
     * This method returns the brand of the article.
     * @return the article's brand.
     */
    String getBrand();
    
    /**
     * This method returns the price of the article.
     * @return the article's price.
     */
    double getPrice();
    
}
L'interface fr.koor.stock.StockArticle

La seconde interface définie les méthodes exposées par le composant de gestion du stock à proprement parler. Bien entendu, ces méthodes acceptent, en paramètres, des instances de StockArticle. En voici son code.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
package fr.koor.stock;

/**
 * This interface gives access to stock management.
 */
public interface StockManager {

    /**
     * This method checks the availability of an item in a certain quantity.
     * @param article   The desired article.
     * @param quantity  The desired quantity.
     * @return  This method returns true if desired articles are available, false otherwise. 
     */
    boolean isAvailable( StockArticle article, int quantity );
    
    /**
     * This method removes from stock some articles.
     * @param article   The article to remove.
     * @param quantity  The quantity to remove.
     * @return The remaining article's quantity, after remove.
     */
    int removeFromStock( StockArticle article, int quantity );
    
    /**
     * This method adds from stock some articles.
     * @param article   The article to add.
     * @param quantity  The quantity to add.
     * @return The new article's quantity.
     */
    int addToStock( StockArticle article, int quantity );

}
L'interface fr.koor.stock.StockArticle

Considérons maintenant le code suivant qui utilise un composant de type StockManager.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
package fr.koor.command;

import java.util.Map;

import fr.koor.stock.StockArticle;
import fr.koor.stock.StockManager;

public class CommandGenerator {

    private StockManager stockManager;
    
    public void setStockManager(StockManager stockManager) {
        if ( stockManager == null ) {
            throw new NullPointerException( "stockManager cannot be null" );
        }
        this.stockManager = stockManager;
    }
    
    public double createFakeCommand( Map<StockArticle, Integer> rawData ) {
        // On vérifie que tous les articles désirés sont présents dans le stock
        for ( StockArticle article : rawData.keySet() ) {
            int quantity = rawData.get( article );
            if ( ! stockManager.isAvailable( article, quantity ) ) {
                throw new RuntimeException("This command is not complete");
            }
        }

        // Si c'est le cas, on retire tous ces articles du stock
        // et on calcule le prix total de la commande.
        double price = 0;
        for ( StockArticle article : rawData.keySet() ) {
            int quantity = rawData.get( article );
            price += quantity * article.getPrice();
            stockManager.removeFromStock( article, quantity );
        }
        
        // On renvoie le prix total de la commande.
        return price;
    }
    
}
L'interface fr.koor.command.CommandGenerator

Ce TP consiste à tester, avec JUnit 5, le code présenté ci-dessous en bouchonnant, via Mockito, notre API de gestion de stock. Vous devez y coder deux tests : le premier qui simule une commande avec les articles disponibles dans le stock et un second qui simule des articles indisponibles. Bon courage !

encore une fois, jouez le jeu et ne passez pas directement à la correction ;-)

La correction

Voici donc le code JUnit/Mockito permettant de tester la création de la commande.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
package fr.koor.mock;

import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import fr.koor.command.CommandGenerator;
import fr.koor.security.SecurityManagerException;
import fr.koor.stock.StockArticle;
import fr.koor.stock.StockManager;

public class CommandGeneratorTest {

    // On déclare nos cinq mocks pour nos 5 interfaces.
    @Mock
    private StockArticle article1;
    
    @Mock
    private StockArticle article2;
    
    @Mock
    private StockManager stockManager;
    
    private Map<StockArticle, Integer> rawData;
    
    
    @BeforeEach
    public void setUp() throws SecurityManagerException {
        // On instancie et on injecte les mocks.
        MockitoAnnotations.openMocks(this);
        
        // Et on configure les mocks d'articles.
        Mockito.when( article1.getPrice() ).thenReturn( 10.0 );
        Mockito.when( article2.getPrice() ).thenReturn( 100.0 );
        
        // On prépare les données de la commande.
        rawData = new HashMap<>();
        rawData.put( article1, 5 );
        rawData.put( article2, 2 );
    }
    
    @Test 
    public void testGoodCommand() throws Exception {
        // On configure notre mock pour simuler des articles disponibles dans le stock.
        Mockito.when( stockManager.isAvailable(article1, 5) ).thenReturn( true );
        Mockito.when( stockManager.isAvailable(article2, 2) ).thenReturn( true );

        // On lance notre test.
        CommandGenerator codeToValidate = new CommandGenerator();
        codeToValidate.setStockManager( stockManager );
        double totalPrice = codeToValidate.createFakeCommand( rawData );
        
        // On vérifie que tout a fonctionné correctement. 
        Assertions.assertEquals( 250, totalPrice );
        Mockito.verify( stockManager, Mockito.times( 1 ) ).removeFromStock( article1, 5 );
        Mockito.verify( stockManager, Mockito.times( 1 ) ).removeFromStock( article2, 2 );
    }

    @Test 
    public void testBadCommand() throws Exception {
        Assertions.assertThrows( RuntimeException.class, () -> {
            // On configure notre mock pour simuler des articles indisponibles dans le stock.
            Mockito.when( stockManager.isAvailable(article1, 5) ).thenReturn( true );
            Mockito.when( stockManager.isAvailable(article2, 2) ).thenReturn( false );
    
            // On lance notre test.
            CommandGenerator codeToValidate = new CommandGenerator();
            codeToValidate.setStockManager( stockManager );
            codeToValidate.createFakeCommand( rawData );
            
            // On vérifie que tout a fonctionné correctement. 
            Mockito.verify( stockManager, Mockito.times( 0 ) ).removeFromStock( article1, 5 );
        });
    }
    
}
Code JUnit/Mockito de test de notre commande avec le stock bouchonné.

Pour aller plus loin

Si vous souhaitez continuer à tester l'utilisation de mocks, vous pouvez aussi refaire les dernières manipulations (bouchonnage du composant de sécurité et celui de gestion du stock) avec JMock et EasyMock. Bon courage :-)



Code Coverage Pourquoi utiliser une API de logging ?