Mockito Spy : simuler une partie de vos classes

Mockito

Pour faciliter l'écriture de tests unitaires, nous utilisons des objets mocks pour simuler le comportement des classes utilisées par la méthode à tester.

Cependant, il arrive des situations où nous avons besoin de simuler le comportement de méthodes au sein même de la classe contenant la méthode à tester. Mockito permet de le faire à l'aide de la méthode Mockito.spy.

En fait, Mockito.spy prend en paramètre l'instance de la classe à tester et retourne un "proxy" sur cette classe. Ceci permet de définir des comportements sur une partie de ses méthodes. Nous pouvons ainsi indiquer qu'une de ces méthodes n'effectue aucune action, retourne une valeur déterminée ou bien lève une exception. Par défaut, si aucun comportement n'est défini sur une méthode alors il s'agit de cette vraie méthode qui sera appelée dans le test.

Nous prendrons comme exemple, une classe de service d'envoi de mails qui contient une méthode principale, une méthode de validation du mail à envoyer et une méthode d'envoi des mails.

Nous souhaitons simuler le comportement de ces deux méthodes sans avoir encore à écrire leur implémentation et sans avoir à les externaliser dans d'autres classes qui seraient ensuite mockées ensuite dans le test. Nous voulons tout dans la même classe de service afin de limiter le nombre de classes.

Voici comment définir les comportements des méthodes simulées en utilisant Mockito.spy :

Simuler le comportement d'une méthode ne retournant pas de valeur

Pour une méthode ne retournant pas de valeur, nous indiquons dans un premier temps à Mockito que cette méthode doit être mockée et qu'elle n'effectue aucune action, puis nous vérifions que cette méthode a bien été appelée.

Pour cela, la méthode "Mockito.doNothing().when(...)" est utilisée pour définir le comportement de cette méthode et "Mockito.verify()" est utilisée pour vérifier que la méthode a bien été utilisée.

Nous avons par exemple le service suivant qui contient une méthode de vérification du mail à envoyer et une méthode d'envoi de mail. Ces deux méthodes ne retournent aucune valeur :

public class MailService {

	public void validateAndSend(final Mail mail) {
		this.validate(mail);
		this.send(mail);
	}

	public void validate(final Mail mail) {
		// Vérification
	}

	public void send(final Mail mail) {
		// Envoi
	}

}

Le test suivant vérifie que les deux méthodes de validation et d'envoi du mail sont bien appelées toutes les deux :

public class MailServiceTest {
    @Test
    public void test() {
        MailService mailService = new MailService();
        mailService = Mockito.spy(mailService);
        
        Mail mail = Mockito.mock(mail);
        
        Mockito.doNothing().when(mailService.validate(mail));
        Mockito.doNothing().when(mailService.send(mail));
        
        // test
        mailService.validateAnsSendMail(mail);
        
        Mockito.verify(mailService).validate(mail);
        Mockito.verify(mailService).send(mail);
    }
}

La présence des "Mockito.doNothing().when(...)" permettent d'indiquer à Mockito que ces méthodes n'effectue aucune action. L'appel aux méthodes "Mockito.verify(mock).methode()" permettent de vérifier que ces méthodes ont bien été appelées.

Ceci permet de vérifier que ces méthodes ont été appelées mais nous n'avons pas eu à tester le contenu de ces méthodes, ni à déplacer ces méthodes dans une autre classe que nous aurions eu à mocker.

Simuler le comportement d'une méthode retournant une valeur

Pour une méthode retournant une valeur, nous l'indiquons dans un premier temps que cette méthode est mockée et qu'elle retourne une valeur en fonction du paramètre.

Pour cela nous utilisons la méthode "Mockito.doReturn(...).when(...)" pour indiquer la valeur retournée par cette méthode en fonction du paramètre.

Nous avons le service suivant dont la méthode de validation retourne un booléen indiquant si le mail est valide ou non :

public class MailService {

	public void validateAndSend(final Mail mail) {
		if (this.validate(mail)) {
			this.send(mail);
		}
	}

	public boolean validate(final Mail mail) {
		// Vérification
		return false;
	}

	public void send(final Mail mail) {
		// Envoi
	}

}

Le test suivant simule les deux cas où le mail est validé ou non par la méthode de validation. Ce test vérifie que la méthode d'envoi du mail est effectué ou non suivant que la validation a réussi ou non :

public class MailServiceTest {

	@Test
	public void test_validation_ok() {
		MailService mailService = new MailService();
		mailService = Mockito.spy(mailService);

		final Mail mail = Mockito.mock(Mail.class);

		Mockito.doReturn(true).when(mailService).validate(mail);
		Mockito.doNothing().when(mailService).send(mail);

		// Test
		mailService.validateAndSend(mail);

		Mockito.verify(mailService).send(mail);
	}

	@Test
	public void test_validation_ko() {
		MailService mailService = new MailService();
		mailService = Mockito.spy(mailService);

		final Mail mail = Mockito.mock(Mail.class);

		Mockito.doReturn(false).when(mailService).validate(mail);
		Mockito.doNothing().when(mailService).send(mail);

		// Test
		mailService.validateAndSend(mail);

		Mockito.verify(mailService, Mockito.never()).send(mail);
	}

}

Simuler le comportement d'une méthode levant une exception

Pour indiquer que la méthode simulée lève une exception, nous utilisons la méthode "Mockito.doThrow(exception).when(...)" qui indique que la méthode lève une exception.

Nous avons ainsi le service suivant dont la méthode "validate(mail)" qui lève une exception en cas d'erreur de validation :

public class MailService {

	public void validateAndSend(final Mail mail) {
		try {
			this.validate(mail);
			this.send(mail);
		} catch (final MailValidationException e) {
			// Gérer l'exception
		}
	}

	public void validate(final Mail mail) throws MailValidationException {
		// Vérification
	}

	public void send(final Mail mail) {
		// Envoi
	}

}

Le test suivant simule une levée d'exception par la méthode de validation. Nous vérifions ensuite que l'envoi du mail n'a pas été effectué :

public class MailServiceTest {

	@Test
	public void test_validation_exception() throws MailValidationException {
		MailService mailService = new MailService();
		mailService = Mockito.spy(mailService);

		final Mail mail = Mockito.mock(Mail.class);

		Mockito.doThrow(new MailValidationException()).when(mailService).validate(mail);
		Mockito.doNothing().when(mailService).send(mail);

		// Test
		mailService.validateAndSend(mail);

		Mockito.verify(mailService, Mockito.never()).send(mail);
	}

}

Conclusion

L'utilisation de Mockito.spy permet de simuler le comportement de méthodes à l'intérieur de la classe à tester.

Ceci évite d'avoir à tester en une seule fois les méthodes de la même classe qui seraient appelées entre elles. Ceci permet de diminuer cette complexité et d'avoir à gérer tous les cas de tests possibles.

Ceci permet également d'éviter d'externaliser les méthodes dans d'autres classes qui seraient mockées dans le test.

Référence

2 commentaires

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.