Mocks aren't stubs - Développement piloté par les tests et doublures d’objets


Les différentes doublures d'objet

On parle de doublure en référence à celles du cinéma. Une doublure d'objet prendra la place d'une instance complètement implémentée, uniquement pour passer un test. Il existe quatre types de doublures :

Les deux doublures les plus utilisées sont les stubs et les mocks. Ils sont souvent confondus, plus en raison de leurs utiilisations que de leurs implémentations. Un stub est écrit pour "boucher les trous" et éviter de bloquer le développement. Un mock, étant donné la nécessité de surveiller l'objet et d'en prendre le contrôle pour retourner une valeur sur un appel spécifique, l'implémentation est plus complexe. Classe anonyme, proxy et réflexivité sont les outils généralement utilisés par les frameworks de mocking. Ce n'est pas le sujet ici, mais lire le code source des frameworks demeure tout à fait instructif.

Avant d'expliquer en quoi l'utilisation des stubs et des mocks est vraiment différente, une question subsiste : pourquoi utiliser une doublure ?


Pourquoi utiliser une doublure d'objet ?

Il était question de la testabilité dans l'introduction. Le premier usage que l'on peut faire d'une doublure, est de tester unitairement une fonctionnalité qui a une dépendance avec une entité externe à l'application, comme une base de donnée ou un mailer. On ne souhaite pas envoyer un email à chaque exécution de test. Il est alors nécessaire d'abstraire le mailer et d'utiliser une doublure lors des tests.

Replaçons nous dans l'exemple de l'entrepôt et de la commande. Nous avons besoin d'une nouvelle fonctionnalité : lorsqu'une commande est effectuée mais qu'il n'y a plus assez de stock pour celle-ci, un email doit être envoyé. Un nouveau test est alors écrit. Pour le test classique il est nécessaire d'écrire un stub que voici :

			
public class MailServiceStub implements MailServiceInterface {
	private List messages = new ArrayList();

	public void send(Message message) {
		messages.add(message);
	}

	public int numberSent() {
		return messages.size();
	}
}
			
			

Envoyer un mail pour ce stub consiste à ajouter le message dans une collection de messages, ce qui incrémente le nombre de messages virtuellement envoyés. Il est alors possible d'écrire le test ci-dessous, sans crainte d'envoyer réellement un mail via l'application :

			
			...
@Test
public void testOrderSendsMailIfUnfilled() {
	Order order = new Order(GUINESS, 51);
	MailServiceStub mailer = new MailServiceStub();
	order.setMailer(mailer);
	order.fill(warehouse);
	assertEquals(1, mailer.numberSent());
}
			...
			
			

Grâce à l'assertion assertEquals(1, mailer.numberSent()), on s'assure qu'un mail a bien été envoyé après l'appel de la méthode fill() sur une commande demandant plus de produit qu'il y'en a dans l'entrepôt.

Le même test mais en utilisant un mock à la place d'un stub s'écrirait ainsi :

			
@Test
public void testOrderSendsMailIfUnfilled() {
	Order order = new Order(GUINESS, 51);
	final WarehouseInterface warehouseMock = context.mock(WarehouseInterface.class);
	final MailServiceInterface mailerMock = context.mock(MailServiceInterface.class);
	order.setMailer(mailerMock);

	context.checking(new Expectations() {{
		oneOf(warehouseMock).hasInventory(with(any(String.class)), with(any(int.class))); will(returnValue(false));
		oneOf(mailerMock).send(with(any(Message.class)));
	}});

	order.fill(warehouseMock);
}
			
			

Comme expliqué précédemment, toute la logique des mocks se situent dans les Expectations. Il y'a ici deux appels principaux, mis en couleur dans le code. Ces lignes pourraient être traduit comme ci-desous :

Nous constatons donc deux différences majeures avec le stub :

Ces doublures de test, dans un cas classique comme orienté mock, apporte plus de testabilité. Mais la meilleure façon de garantir qu'un test soit testables, c'est d'écrire le test avant la fonctionnalité. Il s'agit du fameux Test Driven Development ou TDD, évoqué en conclusion de la partie précédente.


Le développement piloté par les tests

Cette méthodologie mériterait un exposé très démonstratif, mais il est essentiel d'en parler ici. Le TDD consiste à écrire les tests d'une fonctionnalité, puis, de la coder. Certes, dit ainsi, cela semble aussi simple que superflu. C'est en réalité plus complexe.

Comment faire du TDD ?

Il s'agit d'un processus itératif :

  1. Écrire un test et l'exécuter pour qu'il échoue : cela permet de vérifier que le test n'est pas systématiquement valide, auquel cas il ne testerait rien.
  2. Écrire le code applicatif permettant de faire passer le test : il s'agit donc de corriger le test.
  3. Refactoriser le code et relancer le test : la correction du test doit être simpliste, pour converger vers un programme fonctionnel. Cela permet de s'assurer de la valeur du test. Il est ensuite possible de remanier le code pour qu'il soit le plus qualitatif possible, puis de relancer le test qui servira de garde fou. Cela permet de refactoriser sans casser le code.
  4. Refactoriser le test et le relancer : la qualité du code constituant les tests est toute aussi important que celle du code applicatif. Un test lisible et simple, c'est un test facile à maintenir et qui - potentiellement -, décrit une fonctionnalité bien codée et facile à comprendre.
  5. Recommencer l'étape une jusqu'à ce qu'il n'y ait plus de fonctionnalité à coder.

Pourquoi faire du TDD ?

Dans quel contexte coder en TDD ?

Sans rentrer dans les détails, le TDD s'intègre parfaitement dans un contexte de développement utilisant les méthodes agiles. Les bonnes pratiques sont : partir d'une User Story, écrire le test et le code en TDD - en Pair Programming ou non -, et opter pour une solution d'intégration continue. Il en résulte une application construite petit à petit, toujours concentrée sur la valeur fonctionnelle sans négliger la qualité technique, et testée à chaque modification envoyée sur le logiciel de suivi de version.

Deux styles de test pour deux styles de TDD

L'utilisation de tests classiques ou orientés mock avec le TDD révèle les différences entre les deux techniques.