Mockito

Mockito est un framework Java qui permet de de réaliser de « vrais tests unitaires » en isolant la classe que l’on souhaite tester en remplacant ses dépendances par des mocks (ou simulacres). Ces mocks sont des objets simulés qui reproduisent le comportement d’objets réels de manière contrôlée.

Je rapelle que les tests unitaires sont à la base du bon fonctionnement d’une application et sont à la charge et à la responsabilité du développeur.

Article intéressant sur TDD : http://www.cftl.fr/index.php?id=78

Je parle de « vrais tests unitaires » car on confond souvent test unitaire et test d’intégration.
On ne compte plus le nombre de projets qui pensent faire du test unitaire et qui font en fait du test d’intégration !
Un test unitaire permet de vérifier qu’une méthode, prise individuellement, fait bien ce qu’elle est censée faire.
Un test d’intégration permet de démontrer que les différentes parties de l’application fonctionnent bien ensemble. Il faut, en général, des moyens bien plus importants (un jeu de données conséquent, des services externes…) pour un test d’intégration.

Test unitaire

Un exemple :
AdressDAO permet d’accéder aux entités de type Address
UserDAO permet d’accéder aux entités de type User
JMSTemplate est un service Spring qui va me servir à envoyer des messages dans une queue.
UserManagementService permet de gérer les utilisateurs et dépend, entre autres, de AdressDAO de UserDAO et de JMSTemplate.

Les classes AdressDAO et UserDAO possèdent chacune leur propre classe de tests unitaires avec un taux de couverture satisfaisant.
On peut considérer que le service JMSTemplate fourni par Spring est déjà testé.
Dans la classe de tests unitaires de UserManagementService il est donc inutile (et complexe) de retester AdressDAO, UserDAO et JMSTemplate. Il faut donc créer des mocks pour ces deux dépendances afin de ne vérifier que le code métier de notre service.

Débuter avec Mockito

Dans le cas d’un projet Maven, il suffit d’ajouter la dépendance suivante dans votre fichier pom

<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-all</artifactId>
   <version>1.9.5</version>
   <scope>test</scope>
</dependency>

Il faut ensuite greffer Mockito sur votre classe de test.
Il est possible de modifier le Runner comme cela :

@RunWith(MockitoJunitRunner.class)
public class MyTestClass {
}

ou bien de faire une initialisation dans une méthode de la classe annotée avec @Before :

@Before
public void init() {
    MockitoAnnotations.initMocks(this);
}

Créer et injecter des mocks

L’annotation @Mock permet de créer un nouveau mock :

@Mock
private UserDAO userDAOMock;

Le mock sera initialisé par le Runner ou par la méthode initMocks

L’annotation @InjectMocks permet d’injecter les mocks créés dans le service :

@InjectMocks
private UserManagementServiceImpl userManagementService;

Ici, userManagementService est instancié par Mockito. S’il y’avait des attributs annotés @Value par exemple, ces derniers n’étant pas inutilisés par Spring, ils auront la valeur null. Voici un contournement, utilisant la réflexion, qui permet d’initialiser la valeur de notre attribut :

ReflectionTestUtils.setField(userManagementService, "myAttribute", myAttribute);

Une autre solution est d’utiliser directement le bean Spring :

@Autowired
@InjectMocks
private UserManagementService userManagementService;

Note : Gardez en mémoire que vous allez définir des mocks pour l’unique instance Spring du service. Veuillez donc à ne pas utiliser cette instance à un autre endroit pour le contexte courant.

Définir le comportement d’un mock

On peut définir facilement un comportement pour une méthode donnée.
La manière la plus simple est de définir la valeur de retour d’une méthode en fonction de paramètres en entrée.
Pour cela on utilise la combinaison when/thenReturn

// Ici, isAdmin() retournera toujours vrai si elle est appelée avec "userA" et toujours faux si elle est appelée avec "userB"
when(userDAOMock.isAdmin("userA").thenReturn(true);
when(userDAOMock.isAdmin("userB").thenReturn(false);

// Ici, isAdmin() retournera toujours vrai quelque soit l'argument passé en paramètre
when(userDAOMock.isAdmin(anyString()).thenReturn(true);

// Ici, isAdmin() retournera vrai puis toujours faux quelque soit l'argument passé en paramètre
when(userDAOMock.isAdmin(anyString()).thenReturn(true, false);

// Ici, isAdmin() lèvera une exception si l'argument passé en paramètre est null
when(userDAOMock.isAdmin(null).thenThrow(new UserException("User not found"));

Cette façon de faire ne marchera pas pour les méthodes void. Il faudra procéder comme cela :

doThrow(new UserException("User not found")).when(userDAOMock).myvoidmethod(null);

Matchers

anyString() que l’on vient de voir est un Matcher. Il permet de cibler n’importe quel objet de type String.
Il existe une multitude de Matchers anyInt(), anyCollection()… et aussi le très pratique any(Class clazz) qui permet de cibler n’importe quel objet en fonction de la classe passée en paramètre.

Pour aller encore plus loin, Mockito propose ArgumentMatcher :

// Ici, on ne cible que les arguments qui ont plus de 5 caractères.
argThat(new ArgumentMatcher<String>() {
	@Override
	public boolean matches(Object arg) {
		return (StringUtils.length((String) arg) > 5);
	}
});

Answers

Answer permet de définir le comportement interne d’une méthode.
Si votre code est bien pensé vous ne devriez pas trop avoir affaire à ce genre de réponse.

// Ici, on retourne directement l'argument qui a été passé en argument.
when(userDAOMock.persist(any(User.class))).thenReturn(returnsFirstArg());

// Ici, on modifie l'argument passé en paramètre avant de le retourner.
when(userDAOMock.persist(any(User.class))).thenAnswer(new Answer<User>() {
	@Override
	public User answer(InvocationOnMock invoc){
		User user = (User) invoc;
		user.setId(1L);
		return user;
	}
});

Vérifier le bon déroulement de la méthode que l’on teste

Une fois que le comportement des mocks a été défini il faut bien entendu vérifier le bon déroulement de l’appel de la méthode que vous êtes en train de tester.
La valeur de retour de la méthode peut-être vérifiée de manière classique avec JUnit.
De son coté, Mockito offre la possibilité de vérifier que les mocks on bien été appelés le bon nombre de fois avec les bons arguments.

Vérifier l’appel à un mock

// Ici, on vérifie que la méthode isAdmin() a bien été appelée 1 fois avec l'argument "userA"
// La vérification peut également se faire avec un Matcher (vu plus haut)
verify(userDAOMock).isAdmin("userA")

// Ici, on vérifie que la méthode update() n'a jamais été appelée
verify(userDAOMock, never()).update(any(User.class));

// Ici, on vérifie que la méthode increment() a été appelée 2 fois
verify(userDAOMock, times(2)).increment();

// Ici, on vérifie que la méthode increment() a été appelée au moins 2 fois
verify(userDAOMock, atLeast(2)).increment();

// Ici, on vérifie que les appels sont exécutés dans un certain ordre
InOrder inOrder = Mockito.inOrder(addressDAOMock, userDAOMock);
inOrder.verify(addressDAOMock).create(any(Address.class));
inOrder.verify(userDAOMock).create(any(User.class));

Note : Le nombre d’appels à une méthode dépend souvent de la façon dont cette dernière a été implémentée. C’est à vous de juger dans quels cas il est intéressant de le vérifier.

ArgumentCaptor

Les capteurs d’arguments permettent de vérifier l’argument d’un appel de manière plus simple qu’un Matcher. L’argument d’un appel est dans un premier temps capturé afin d’être testé de manière classique avec JUnit.

ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(userDAOMock).update(captor.capture());

User user = captorProc.getValue();
assertEqual("userB", user.getUsername());
assertEqual(3, user.getRoles().size());
assertTrue(user.isEnabled());

Vous avez ce qu’il faut pour bien débuter, à vous de jouer !

Publicités