Développons en Java 2.30 | |
Copyright (C) 1999-2022 Jean-Michel DOUDOUX | (date de publication : 15/06/2022) |
|
Niveau : | Supérieur |
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.
Ce chapitre contient plusieurs sections :
En POO, il existe plusieurs types d'objets, généralement appelés doublures, permettant de simuler le comportement d'un autre objet :
Le vocabulaire lié à ces types d'objets est assez confus dans la langue anglaise donc il l'est d'autant plus dans la langue française où l'on tente de le traduire. Ce chapitre va se concentrer essentiellement sur les objets de type mock.
Un objet de type doublure permet donc de simuler le comportement d'un autre objet concret de façon maitrisée.
L'emploi de doublures est largement utilisé pour les tests unitaires mais il peut aussi être mis en oeuvre lors des développements pour par exemple remplacer un objet qui n'est pas encore écrit.
L'utilisation des doublures permet aux tests unitaires de se concentrer sur les tests du code de la méthode qui correspond au System Under Test (SUT) sans avoir à se préoccuper des dépendances.
Les doublures ont pour rôle de simuler le comportement d'un objet permettant ainsi de réaliser les tests de l'objet de façon isolée et répétable.
Un objet de type mock permet de simuler le comportement d'un autre objet concret de façon maitrisée et de vérifier les invocations qui sont faites de cet objet.
Cette double fonctionnalité permet dans un test unitaire de faire des tests sur l'état (state test) et des tests sur le comportement (behavior test).
Il existe deux grands types d'objets mock :
Les objets mock peuvent être codés manuellement ou utiliser un framework qui va permettre de les générer dynamiquement. L'avantage des mocks dynamiques c'est qu'aucune classe implicite n'a besoin d'être écrite.
Les frameworks de mocking peuvent utiliser plusieurs solutions pour mettre en oeuvre des mocks dynamiques :
Avec l'utilisation de proxies, il est indispensable d'avoir un mécanisme d'injection de dépendances permettant de fournir l'implémentation à utiliser. Ceci permet dans le cas des tests unitaires de fournir un objet de type mock qui sera utilisé lors de l'exécution des tests à la place d'une vraie instance de classe dépendante.
Ce mécanisme d'injection de dépendances peut être fourni par un framework (exemple : Spring) ou implémenté manuellement mais dans tous les cas le code à tester doit fournir une solution pour le réaliser.
Il existe plusieurs frameworks de mocking en Java qui permettent de créer dynamiquement des objets de type mock.
Dans une application, les classes ont généralement des dépendances entre elles. Ceci est particulièrement vrai dans les applications développées en couches (présentation, service, métier, accès aux données (DAO), ...).
L'idée lors de l'exécution d'un test unitaire est de tester la plus petite unité de code possible, soit la méthode et uniquement le code de la méthode. Cependant les classes utilisées dans le code de cette méthode font généralement appel à un ou plusieurs autres objets. Le but n'est pas de tester ces objets qui feront eux-mêmes l'objet de tests unitaires mais de tester le code de la méthode : le test unitaire doit concerner uniquement la méthode et ne pas tester les dépendances.
Il faut donc une solution pour s'assurer que les objets dépendants fournissent les réponses désirées à leur invocation. Cette solution repose sur les objets de type simulacre.
Cela suppose que si le code de la méthode fonctionne comme voulu (validé par des tests unitaires) et que les dépendances fonctionnent de même (validées par leurs tests unitaires) alors ils fonctionneront normalement ensembles.
Les classes dépendantes ne doivent pas être testées dans les tests unitaires de la classe. Elles doivent être considérées comme testées, sachant que des tests unitaires qui leur sont dédiés doivent exister. Certaines classes doivent aussi être considérées comme testées : c'est notamment le cas des classes du JRE.
Il est très important que les tests unitaires ne concernent que le code de la méthode en cours de test. Autrement, il est difficile de trouver un bug qui peut être dans un objet dépendant de niveau -N.
Il est alors nécessaire de simuler le fonctionnement des classes dépendantes.
Le but d'un objet Mock est de remplacer un autre objet en proposant de forcer les valeurs de retour de ses méthodes selon certains paramètres.
Ainsi l'invocation d'un objet de type mock garantit d'avoir les valeurs attendues selon les paramètres fournis.
Un des avantages à utiliser des objets mock, notamment dans les tests unitaires, est qu'ils forcent le code à être écrit ou adapté par des refactoring pour qu'il respecte une conception permettant de le rendre testable.
Généralement, un objet de type mock est une implémentation d'une interface qui se limite le plus souvent à renvoyer des valeurs déterminées en fonction des paramètres reçus. L'interface est parfaitement adaptée puisque l'objet simulé et l'objet mock doivent avoir le même contrat.
Un objet de type mock possède donc la même interface que l'objet qu'il doit simuler, ce qui permet d'utiliser le mock ou une implémentation concrète de façon transparente pour l'objet qui l'invoque.
Les objets mock simulent le comportement d'autres objets mais ils sont aussi capables de vérifier les invocations qui sont faites sur le mock : nombres d'invocations, paramètres fournis, ordre d'invocations, ...
La mise en oeuvre d'un objet de type mock dans les tests unitaires suit généralement plusieurs étapes :
Les objets de type mock peuvent être utilisés dans différentes circonstances :
Les objets de type mock sont donc très intéressants pour simuler le comportement de composants invoqués de façon distante (exemple : EJB, services web, RMI, ...) et particulièrement pour tester les cas d'erreurs (problème de communication, défaillance du composant ou du serveur qui gère leur cycle de vie, ...).
Les tests unitaires automatisés sont une composante très importante du processus de développement et de maintenance d'une application, malgré le fait qu'ils soient fréquemment négligés.
Pour permettre de facilement détecter et corriger d'éventuels bugs dans le code testé, il est nécessaire d'isoler ce code en simulant le comportement de ses dépendances.
L'utilisation des objets mock est une technique particulièrement puissante pour permettre des tests unitaires sur des classes.
Les objets de type mock permettent réellement des tests qui soient unitaires puisque leur résultat est prévisible. Si le test échoue, il y a une forte probabilité que l'origine du problème soit dans la méthode en cours de test. Ceci facilite la résolution du problème puisque celui-ci est isolé à l'intérieur de cette méthode.
Les objets de type mock permettent de s'assurer que l'échec d'un test n'est pas lié à une de ses dépendances sauf si les données retournées par le ou les objets mock sont erronées vis-à-vis du cas de test en échec.
Les objets mock sont particulièrement utiles dans les tests unitaires mais ils sont à éviter dans les tests d'intégration. Le but des tests d'intégration étant de tester les interactions entre les modules et les composants, il n'est pas forcément souhaitable de simuler le comportement de certains d'entre-eux.
Pour les tests d'intégration, les objets mock peuvent cependant être utiles dans certaines circonstances :
Les tests unitaires doivent toujours s'exécuter le plus rapidement possible notamment si ceux-ci sont intégrés dans un processus de build automatique. Un test unitaire ne doit donc pas utiliser de ressources externes comme une base de données, des fichiers, des services, ... Les tests avec ces ressources doivent être faits dans les tests d'intégration puisque ce sont des dépendances.
Les tests utilisant une fonctionnalité dont le résultat est aléatoire ou fluctuant selon les appels avec le même contexte ne sont pas répétables.
Exemple : une méthode qui convertit le montant d'une monnaie dans une autre. La méthode utilise un service web pour obtenir le cours de la monnaie cible. A chaque exécution du cas de test, le résultat peut varier puisque le cours d'une monnaie fluctue.
Pour permettre d'exécuter correctement les tests d'une méthode qui utilise une telle fonctionnalité, il faut simuler le comportement du service dépendant pour qu'il retourne des valeurs prédéfinies selon le contexte fourni en paramètre. Ainsi pour chaque cas de tests, le service retournera la même valeur rendant ainsi les résultats prédictibles et donc les tests répétables.
Bien sûr ce type de tests pose comme pré-requis que le service fonctionne correctement mais cela est du ressort des développeurs du service qui doivent eux aussi garantir le bon fonctionnement de leur service en utilisant des tests unitaires.
Les objets de type mock peuvent aussi permettre de facilement tester des cas d'erreurs. Certaines erreurs sont difficiles à reproduire donc à tester, par exemple un problème de communication avec le réseau, d'accès à une ressource, de connexion à un serveur (Base de données, Broker de messages, système de fichiers partagés,...).
Il est possible d'effectuer des opérations manuelles pour réaliser ces tests (débrancher le câble réseau, arrêter un serveur, ...) mais ces opérations sont fastidieuses et peu automatisables.
Il est par contre très facile pour un objet mock de retourner une exception qui va permettre de simuler et de tester le cas d'erreur correspondant.
Le principe de limiter les responsabilités d'un objet pour faciliter la réutilisation implique qu'un objet a souvent besoin d'autres objets pour réaliser ses tâches. Ces objets dépendants ont eux aussi des dépendances vers d'autres objets. Ces dépendances forment rapidement un graphe d'objets complexe qui pose rapidement des problèmes pour les tests et surtout qui empêche l'isolation du test de l'objet à tester. Cela devient particulièrement vrai si une ou plusieurs de ces dépendances utilisent des ressources distantes, longues à répondre ou dont les résultats ne sont pas constants.
Le test unitaire d'un objet peut utiliser des objets de type mock pour simuler le comportement de leurs dépendances immédiates.
Seules les dépendances de premier niveau ont besoin d'être remplacées par des objets mock pour tester l'objet. Pour tester l'objet, il est inutile de créer des mocks pour les dépendances de niveaux 2 et supérieurs.
Un code mal conçu pour être testable est un frein à la rédaction des tests unitaires automatisés car ceux-ci seront trop compliqués voire impossibles à écrire.
Les tests unitaires, et plus encore, l'utilisation d'objets de type mock encourage voire impose une conception adaptée du code qui, au final, le rend non seulement testable mais aussi plus compréhensible et plus maintenable.
L'écriture de tests unitaires impose que le code écrit soit testable, ce qui implique notamment que :
Si une classe dépendante est simplement instanciée dans le code de la méthode à tester, il ne sera pas possible de la simuler en la remplaçant par un autre objet. Pour faciliter les tests unitaires qui utilisent des objets mocks, il faut mettre en oeuvre un mécanisme d'injection de dépendances et définir une interface pour chaque objet dépendant.
Les dépendances doivent être décrites par une interface pour permettre facilement de créer des objets de type mocks. Il est possible de créer une instance d'une interface sans avoir à fournir d'implémentations des méthodes. Les objets mocks vont utiliser cette fonctionnalité pour fournir une implémentation qui va simuler le comportement du véritable objet.
L'utilisation de certaines fonctionnalités ou motifs de conception peut entraver, voire rendre compliqué et même impossible le test du code avec des tests unitaires, par exemple :
Si l'accès à une ressource est codé de façon statique, cette ressource devra être disponible lors de l'exécution des tests quand elle sera sollicitée.
Il est généralement préférable d'effectuer un refactoring pour rendre le code testable plutôt que d'écrire des tests unitaires compliqués ou de ne pas les écrire du tout. Au final, le code est amélioré.
Les objets de type mock permettent de réaliser des tests unitaires dans le contexte d'un système reposant sur des développements orientés objets. Dans un tel contexte, il existe des dépendances plus ou moins nombreuses entre les objets.
Ainsi la méthode d'une classe, qui est l'unité à tester, dépends généralement d'un ou plusieurs objets dépendant eux aussi d'autres objets. L'invocation de la méthode lors de son test va inévitablement faire appel à ces dépendances : ceci n'est alors plus un test unitaire mais un test d'intégration. De plus, cela complexifie généralement la détermination de l'origine d'un problème et induit des difficultés de répétatibilité de l'exécution des tests.
Les objets de type mock permettent de maitriser le fonctionnement des dépendances en simulant de façon prédéterminée leur comportement lors de l'exécution des cas de tests.
Les objets de type mock sont donc conçus pour répondre au besoin des tests unitaires.
L'entité testée ne doit pas savoir si l'objet utilisé durant les tests est un mock. Pour cela, les dépendances d'une entité testée doivent être décrites avec une interface et l'entité doit utiliser un mécanisme d'injection pour permettre d'utiliser une implémentation de la dépendance en production et d'utiliser un mock pour les tests.
Le fait de décrire les fonctionnalités d'une dépendance avec une interface facilite la mise oeuvre d'un objet de type mock.
L'injection de dépendances doit permettre de substituer l'implémentation de la dépendance à un objet de type mock. Si le code à tester instancie en dur la dépendance en utilisant l'opérateur new, il est extrêmement difficile de réaliser la substitution sauf en mettant en oeuvre des fonctionnalités avancées et complexes (avec un classloader par exemple).
L'injection peut se faire de différentes façons, par exemple :
Cette section va faire évoluer une classe dont une méthode à tester utilise une dépendance directement instanciée.
L'important pour pouvoir utiliser les mocks c'est que la classe ait été conçue et développée pour être testable. Ceci implique notamment de proposer un mécanisme permettant de pouvoir remplacer une implémentation d'une dépendance par un objet de type mock.
Exemple : |
public class ClasseA {
public String maMethode(){
// début des traitements
ClasseB classeB = new ClasseB();
// suite des traitements utilisant classeB
}
} |
Dans l'exemple ci-dessus, le test de la méthode maMethode() va être difficile car l'instanciation de la dépendance se fait en dur dans le code. Il ne va pas être facile de remplacer cette instance par celle d'un objet mock. Une solution consiste à proposer une méthode qui se charge de retourner une instance de la classe ClasseB. Il est important que cette méthode puisse être redéfinie dans une classe fille.
Exemple : |
public class ClasseA {
public String maMethode(){
// début des traitements
ClasseB classeB = creerClasseB();
// suite des traitements utilisant classeB
}
protected ClasseB creerClasseB() {
return new ClasseB();
}
} |
Pour faciliter la création d'un objet mock, il est préférable que chaque objet dépendant implémente une interface qui décrive ses fonctionnalités.
Exemple : |
public class ClasseB implement InterfaceB {
...
} |
L'objet est alors défini comme une implémentation de son interface, il est ainsi plus facile de créer un objet mock car cela évite de dériver la classe.
Exemple : |
public class ClasseA {
public String maMethode(){
// début des traitements
InterfaceB classeB = creerClasseB();
// suite des traitements utilisant classeB
}
protected InterfaceB creerClasseB() {
return new ClasseB();
}
} |
Lors de l'écriture du test, il faut dériver la classe à tester et réécrire la méthode qui instancie la dépendance pour qu'elle renvoie une instance de l'objet mock.
Exemple : |
public class TestClasseA extends TestCase {
public void testMaMethode() {
ClasseA classeA = new ClasseA(){
// reecriture de la méthode pour qu'elle renvoie un mock
protected InterfaceB creerClasseB() {
// renvoie une instance du mock de la classe B
return new MockB();
}
}
String resultat = classeA.maMethode();
// évaluation des résultats du cas de test
}
} |
Cet exemple permet de facilement mettre en oeuvre le principe d'injections de dépendances dans du code qui n'a rien pour mettre en oeuvre ce type de fonctionnalité. Certains frameworks, comme Spring, offrent également cette possibilité.
Les singletons ne permettent pas de remplacer facilement leur unique instance par un objet de type mock.
Il faut encapsuler les dépendances vers des ressources externes dans des entités dédiées pour permettre de les injecter et ainsi utiliser une implémentation à l'exécution et un objet mock lors des tests.
L'écriture d'objets de type mock à la main peut être longue et fastidieuse, de plus, des objets peuvent contenir des bugs comme toute portion de code. Des frameworks ont donc été développés pour créer ces objets dynamiquement et de façon fiable.
La plupart des frameworks de mocking permettent de spécifier le comportement que doit avoir l'objet mock :
Les frameworks de mocking permettent de créer dynamiquement des objets mocks, généralement à partir d'interfaces. Ils proposent fréquemment des fonctionnalités qui vont bien au-delà de la simple simulation d'une valeur de retour :
Plusieurs frameworks relatifs aux objets de type mock existent dans le monde Java, notamment :
Les différents frameworks vont être utilisés pour mocker les dépendances dans les tests unitaires de la classe ci-dessous qui n'a qu'un rôle purement éducatif.
Exemple : |
package fr.jmdoudoux.dej;
public class MonService {
protected Calculatrice creerCalculatrice() {
return new CalculatriceImpl();
}
/**
* Calculer la somme de deux entiers positifs
*
* @param val1
* la premiere valeur
* @param val2
* la seconde valeur
* @return la somme des deux arguments ou -1 si un des deux arguments est
* negatif
*/
public long additionner(int val1, int val2) {
long retour = 0l;
Calculatrice calculatrice = creerCalculatrice();
try {
retour = calculatrice.additionner(val1, val2);
} catch (IllegalArgumentException iae) {
retour = -1l;
}
return retour;
}
/**
* Calculer la somme des deux premiers parametres et soustraire la valeur du troisième
* @param val1
* @param val2
* @param val3
* @return le resultat du calcul
*/
public long calculer(int val1, int val2, int val3) {
long retour = 0l;
Calculatrice calculatrice = creerCalculatrice();
try {
long somme = calculatrice.additionner(val1, val2);
retour = calculatrice.soustraire(somme, val3);
} catch (IllegalArgumentException iae) {
retour = -1l;
}
return retour;
}
} |
Cette classe utilise une dépendance dont les fonctionnalités sont décrites dans une interface.
Exemple : |
package fr.jmdoudoux.dej;
public interface Calculatrice {
public long additionner(int val1, int val2);
public long soustraire(long val1, int val2);
} |
Exemple : |
package fr.jmdoudoux.dej;
public class CalculatriceImpl implements Calculatrice {
@Override
public long additionner(int val1, int val2) {
if (val1 < 0 || val2 < 0) {
throw new IllegalArgumentException("La valeur ne peut pas etre negative");
}
return val1+val2;
}
@Override
public long soustraire(long val1, int val2) {
return val1-val2;
}
} |
Easy Mock est un framework de mocking open source qui permet de créer et d'utiliser des objets de type mock.
EasyMock est un framework simple (composé uniquement d'une douzaine de classes et interfaces) et très puissant qui permet de créer et d' utiliser des objets de type mock.
EasyMock travaille à partir d'interfaces pour créer des objets de type mock mais il propose une extension qui permet de créer des objets de type mock pour des classes.
Les mocks créés par EasyMock peuvent avoir plusieurs utilités allant du simple dummy qui renvoie une valeur au spy qui permet de vérifier le comportement des invocations de méthodes selon les paramètres fournis.
La version utilisée dans cette section est la 2.4. Elle requiert un Java 5 minimum.
Pour l'utiliser, il faut télécharger l'archive sur le site https://easymock.org/ et la décompresser dans un répertoire du système. Il suffit alors d'ajouter le fichier easymock.jar dans le classpath.
La classe principale d'EasyMock est la classe EasyMock : toutes ses méthodes sont statiques. Il est donc possible de faire un import static de cette classe pour pouvoir invoquer ses méthodes sans avoir à les préfixer par le nom de la classe.
Voici un exemple de tests unitaires de la classe MonService qui utilise EasyMock pour simuler le comportement des dépendances.
Exemple : |
package fr.jmdoudoux.dej;
import org.easymock.EasyMock;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class MonServiceTest {
private Calculatrice mock = null;
private MonService monService = null;
@Before
public void setUp() throws Exception {
mock = EasyMock.createMock(Calculatrice.class);
monService = new MonService() {
@Override
protected Calculatrice creerCalculatrice() {
return mock;
}
};
}
@Test
public void testAdditionner() {
long retour = 0l;
EasyMock.expect(mock.additionner(1, 2)).andReturn(Long.valueOf(3l));
EasyMock.replay(mock);
retour = monService.additionner(1, 2);
Assert.assertEquals("La valeur retournee est invalide", 3l, retour);
}
@Test
public void testAdditionnerParametreInvalide() {
long retour = 0l;
EasyMock.expect(mock.additionner(-1, 2)).andThrow(new IllegalArgumentException());
EasyMock.replay(mock);
retour = monService.additionner(-1, 2);
Assert.assertEquals("La valeur retournee est invalide", -1l, retour);
}
@Test
public void testCalculer() {
long retour = 0l;
EasyMock.expect(mock.additionner(20, 30)).andReturn(Long.valueOf(50l));
EasyMock.expect(mock.soustraire(50, 10)).andReturn(Long.valueOf(40l));
EasyMock.replay(mock);
retour = monService.calculer(20, 30, 10);
Assert.assertEquals("La valeur retournee est invalide", 40l, retour);
}
} |
Par défaut, EasyMock ne permet que de créer des objets de type mock pour des interfaces. EasyMock propose une extension pour créer des objets mock à partir d'une classe. Le code à utiliser est similaire hormis qu'il faut importer le package org.easymock.classextension.EasyMock à la place du package org.easymock.EasyMock.
La classe org.easymock.EasyMock permet de créer et utiliser des objets de type mock à partir d'une interface qui précise les fonctionnalités de l'objet à simuler.
La classe org.easymock.classestension.EasyMock permet de créer et utiliser des objets de type mock à partir d'une classe.
La création d'une instance d'un objet de type mock se fait en invoquant la méthode statique createMock() de la classe EasyMock qui attend en paramètre la classe de l'interface.
Exemple : |
mock = EasyMock.createMock(Calculatrice.class);
|
EasyMock propose trois types de mocks :
La définition du comportement des mocks se fait sous la forme enregistrer/rejouer.
Pour préciser le comportement d'une méthode d'un mock qui renvoie une valeur, il faut :
La méthode expect() permet de préciser le comportement attendu en retour de l'invocation d'une méthode.
L'avantage de cette approche qui nécessite l'invocation de la méthode dont le comportement est à simuler est qu'elle permet d'utiliser le code completion et le refactoring de l'IDE utilisé.
Exemple : |
EasyMock.expect(mock.additionner(1, 2)).andReturn(Long.valueOf(3l));
|
EasyMock propose de simuler la levée d'une exception dans la définition du comportement d'une méthode en utilisant la méthode andThrow() qui attend en paramètre l'instance de l'exception à lever.
Exemple : |
EasyMock.expect(mock.additionner(-1, 2)).andThrow(new IllegalArgumentException());
|
Ceci est particulièrement utile pour tester des cas d'erreurs difficiles à automatiser comme une coupure réseau par exemple. Tous les types d'exceptions peuvent être simulés (checked, runtime ou error).
Pour préciser le comportement d'une méthode qui ne retourne aucune valeur (void), il faut simplement invoquer la méthode sur l'instance du mock sans utiliser la méthode expect(). EasyMock va simplement enregistrer le comportement.
Ainsi, si le résultat de l'invocation de la méthode ne renvoie aucune valeur ni ne lève aucune exception, il ne faut pas utiliser la méthode expect() mais simplement invoquer la méthode sur l'objet de type mock.
Certaines méthodes permettent de préciser le nombre d'invocations d'une méthode :
EasyMock permet aussi facilement de définir des comportements différents lors de plusieurs invocations de la même méthode car les appels aux méthodes andReturn(), andThrow() et times() peuvent être chaînés.
Exemple : |
EasyMock.expect(mock.additionner(-1, 2))
.andReturn(Long.valueOf(3l))
.andThrow(new IllegalArgumentException()); |
Le comportement attendu est ainsi enregistré par l'objet mock jusqu'à l'invocation de la méthode replay().
Une fois définis tous les comportements attendus pour le ou les mocks, il faut invoquer la méthode replay() sur l'objet de type Control.
Exemple : |
EasyMock.replay(mock); |
Si la méthode statique replay() de la classe EasyMock n'est pas invoquée et que des comportements de l'objet mock sont définis alors une exception de type IllegalStateException est levée lors de l'utilisation de ce mock.
Exemple : |
java.lang.IllegalStateException: missing behavior definition
for the preceeding method call additionner(20, 30)
at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:30)
at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:61)
at $Proxy5.soustraire(Unknown Source)
at fr.jmdoudoux.dej.MonService.calculer(MonService.java:45)
at fr.jmdoudoux.dej.MonServiceTest.testCalculer(MonServiceTest.java:56)
...
|
Le nom de la méthode replay() peut être source de confusions : en fait, elle ne rejoue pas le comportement de l'objet mock mais elle réinitialise son état interne pour lui permettre d'avoir le comportement attendu lors des futures invocations des méthodes.
L'appel à la méthode replay() permet de placer le mock en situation de reproduire le comportement défini à l'invocation d'une méthode du mock et d'enregistrer ces invocations.
EasyMock n'est pas utile que pour fournir des réponses déterminées à des invocations données : il permet aussi de vérifier les paramètres fournis et l'ordre d'invocations des méthodes.
Une exception est levée si les paramètres utilisés lors de l'invocation ne correspondent pas à ceux définis dans l'objet de type mock.
Résultat : |
java.lang.AssertionError:
Unexpected method call additionner(2, 2):
additionner(1, 2): expected: 1, actual: 0
at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:32)
at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:61)
at $Proxy5.additionner(Unknown Source)
at fr.jmdoudoux.dej.MonService.additionner(MonService.java:24)
at fr.jmdoudoux.dej.MonServiceTest.testAdditionner(MonServiceTest.java:33) |
Cette exception est levée directement par EasyMock et ne nécessite donc aucune assertion explicite.
C'est le mode de fonctionnement pas défaut d'EasyMock. Cependant, parfois ce mode de fonctionnement est trop strict.
Pour définir qu'un comportement ne requiert pas une valeur précise comme paramètre, il faut utiliser la méthode de la classe EasyMock correspondant au type attendu pour préciser la valeur d'un paramètre : anyObject(), anyInt(), anyShort(), anyByte(), anyLong(), anyFloat(), anyDouble() et anyBoolean().
La méthode EasyMock.notNull() permet de préciser qu'une valeur d'un paramètre doit être non null sans fournir plus de précision sur la valeur.
La méthode EasyMock.matches() permet de préciser qu'une valeur d'un paramètre correspond à un certain motif sous la forme d'une expression régulière.
La méthode EasyMock.find() permet de préciser qu'une valeur d'un paramètre doit contenir un sous-ensemble correspondant à un certain motif sous la forme d'une expression régulière.
La méthode EasyMock.lt() permet de préciser qu'une valeur d'un paramètre numérique doit être inférieure à la valeur fournie en paramètre. La méthode EasyMock.gt() permet de préciser qu'une valeur d'un paramètre numérique doit être supérieure à celle fournie en paramètre.
Il peut être important de vouloir vérifier l'ordre d'invocations des méthodes d'un objet mock. Attention cependant, ceci induit un couplage entre l'objet et son mock.
EasyMock ne se contente pas de vérifier les valeurs des paramètres des méthodes invoquées par le mock et les valeurs retournées. Elle vérifie aussi l'ordre d'invocation de ces méthodes et s'assure que seules les méthodes définies dans le comportement son invoquées. Cette vérification n'est pas faite pas défaut : pour l'activer, il faut utiliser la méthode EasyMock.verify() une fois toutes les invocations des méthodes du mock réalisées.
Exemple : |
@Test
public void testCalculer() {
long retour = 0l;
EasyMock.expect(mock.additionner(20, 30)).andReturn(Long.valueOf(50l));
EasyMock.expect(mock.soustraire(50, 10)).andReturn(Long.valueOf(40l));
EasyMock.expect(mock.additionner(40, 60)).andReturn(Long.valueOf(100l));
EasyMock.replay(mock);
retour = monService.calculer(20, 30, 10);
Assert.assertEquals("La valeur retournee est invalide", 40l, retour);
} |
Ce test s'exécute sans problème.
Exemple : |
@Test
public void testCalculer() {
long retour = 0l;
EasyMock.expect(mock.additionner(20, 30)).andReturn(Long.valueOf(50l));
EasyMock.expect(mock.soustraire(50, 10)).andReturn(Long.valueOf(40l));
EasyMock.expect(mock.additionner(40, 60)).andReturn(Long.valueOf(100l));
EasyMock.replay(mock);
retour = monService.calculer(20, 30, 10);
Assert.assertEquals("La valeur retournee est invalide", 40l, retour);
EasyMock.verify(mock);
} |
Ce test échoue car lors de la vérification, le comportement précise une seconde invocation de la méthode additionner() qui n'est pas réalisée dans les traitements.
Une exception est alors levée par EasyMock pour signaler la différence entre le comportement défini et les traitements invoqués.
Exemple : |
java.lang.AssertionError:
Expectation failure on verify:
additionner(40, 60): expected: 1, actual: 0
at org.easymock.internal.MocksControl.verify(MocksControl.java:101)
at org.easymock.EasyMock.verify(EasyMock.java:1570)
at fr.jmdoudoux.dej.MonServiceTest.testCalculer(MonServiceTest.java:59) |
Ceci peut être particulièrement utile dans certaines circonstances.
Le fonctionnement de la méthode verify() dépend du mode de création de l'objet de type mock. Pour rappel, EasyMock propose trois modes de création :
Un objet de type IMocksControl permet de coupler plusieurs objets de type mock. Ce couplage permet de maintenir des relations sur l'ordre du comportement des différents objets mock.
La mise en oeuvre d'EasyMock avec un control nécessite plusieurs instances :
L'interface IMocksControl propose des méthodes similaires pour mettre en oeuvre les mocks, notamment : createMock(), replay() et verify().
La méthode checkOrder() permet de préciser si l'ordre d'invocations des méthodes des mocks doit être vérifié ou non.
La méthode reset() permet de supprimer tous les comportements définis dans les mocks du control.
Les trois méthodes resetToNice(), resetToStrict() et resetToDefault() permettent de supprimer tous les comportements définis dans les mocks du control et de basculer les mocks, respectivement, en mode nice, strict et défaut.
Toutes ces méthodes agissent sur les mocks définis dans le control.
L'exemple de cette section va mocker le comportement d'une classe de type DAO en proposant notamment d'utiliser des objets de type mock pour les classes Connection, PreparedStatement et ResultSet.
Ainsi, il sera possible de tester unitairement la méthode du DAO sans avoir à faire appel à la base de données.
|
La suite de cette section sera développée dans une version future de ce document
|
Le site officiel du projet est à l'url https://site.mockito.org/
Le site officiel du projet est à l'url http://jmock.org/
|
La suite de cette section sera développée dans une version future de ce document
|
Certaines classes de l'API standard sont particulièrement complexes à mocker. MockRunner propose un ensemble de mocks pour quelques-unes de ces classes.
Le site officiel du projet est à l'url http://mockrunner.sourceforge.net/
|
La suite de cette section sera développée dans une version future de ce document
|
La génération d'objets mock n'est pas toujours pratique car elle permet de créer des objets mais ceux-ci doivent être maintenus au fur et à mesure des évolutions du code à tester.
Il est préférable d'utiliser un framework qui va créer dynamiquement les objets mock. L'inconvénient c'est que le code du test unitaire devient plus important, donc plus complexe et ainsi, plus difficile à maintenir.
L'utilisation d'objets de type mock peut coupler les tests unitaires avec l'implémentation des dépendances utilisées. La plupart des frameworks permettent de préciser et de vérifier l'ordre et le nombre d'appels des méthodes mockées qui sont invoquées lors des tests. Si un refactoring est appliqué sur ces méthodes, changeant leur ordre d'invocation, le test devra être adapté en conséquence.
La mise en oeuvre d'objets de type mock doit tenir compte des limites de leur utilisation. Par exemple, elle masque complètement les problèmes d'interactions entre les dépendances. C'est pour cette raison que les tests unitaires sont nécessaires mais pas suffisants. Il peut aussi être intéressant de ne pas mocker systématiquement toutes les dépendances.
|