Comment rendre vos tests unitaires plus propres grâce aux « Builders »?

Publié par Félix-Antoine Bourbonnais le dimanche 1 avril 2012 à 00:00

Motivations

La vaste majorité des tests unitaires que nous écrivons demandent la création d'un contexte initial préalable (le "given").

Or, l'établissement de ce contexte est souvent long et peut rapidement se transformer en bruit (forme de pollution) qui brouille la raison d'être du test (le "when" et le "then") au travers d'une série de lignes de préparation souvent un peu cryptiques.

Afin de remédier à cela, plusieurs solutions classiques existent: utilisation de la méthode d'initialisation (@Before en Java) ou encore les objets mères (Mother Objects). Mais ces techniques ne sont pas toujours appropriées ou ont des désavantages comme nous le constaterons plus tard.

Une autre technique de plus en plus populaire consiste à utiliser une forme du patron Builder afin de construire des données ou un contexte. Cette technique bénéficie maintenant d'outils (frameworks) simplifiant leur création. Pour la suite de cet article, nous démontrerons cependant l'utilisation manuelle afin de bien en faire ressortir le concept.

Un Builder pour les tests

Afin d'illustrer l'utilisation d'un Builder pour les tests, voici un exemple:

Sans Builder

@Test
public void givenACarWith2DoorsAlreadyUnlockedWhenUnlockThenAllDoorsUnlocked() {
// Given (contexte)
Car aCarWith2DoorsUnloked = new Car(Color.RED)

Door driverDoor = new Door(Size.LARGE, Orientation.FRONT_DRIVER);
driverDoor.unlock();
aCarWith2Doors.addDoor(driverDoor);

Door passengerDoor = new Door(Size.LARGE, Orientation.FRONT_PASSENGER);
passengerDoor.unlock();
aCarWith2Doors.addDoor(passengerDoor);

// When
aCarWith2DoorsUnloked.unlock()

// Then
assertFalse("All doors should be unlocked", aCarWith2DoorsUnloked.allDoorLocked());

}

Le code ci-dessus montre certains problèmes:

  • L'initialisation du contexte compte pour environ 60% des lignes du test. Or, ce n'est pas la partie la plus importante et beaucoup des lignes ne sont que du bruit et ne permettent pas d'identifier rapidement quel est le contexte;
  • L'initialisation étant très longue, on doit ajouter des commentaires afin de diviser les trois grandes parties du test.

Il serait possible d'améliorer ce test en déplaçant ce contexte dans une méthode privée ou encore d'utiliser la méthode d'initialisation (@Before). Mais plusieurs problèmes demeurent:

  • Il est fort probable que les autres tests ne demandent pas exactement la même configuration de voiture. Donc des configurations seront toujours nécessaires et la forme ne changera pas;
  • Cela demanderait de créer plusieurs versions de voitures avec plusieurs configurations différentes. Un bon nombre ne seraient utilisées que par une poignée de tests. Il s'agit d'une pollution de la classe de test, de la méthode d'initialisation et cela rend la compréhension des tests plus difficile;
  • La construction de l'état initial est éloignée de son utilisation. Rendant ainsi le test plus difficile à comprendre, car il faut constamment alterner entre la méthode d'initialisation et la méthode de test pour avoir l'exemple complet (le test).

Avec Builder

@Test
public void givenACarWith2DoorsAlreadyUnlockedWhenUnlockThenAllDoorsUnlocked() {
Car aCarWith2DoorsUnlocked = aCar().withDoors(
aDoor().unlocked().build(),
aDoor().unlocked().build()
).build();

aCarWith2DoorsUnloked.unlock()

assertFalse("All doors should be unlocked", aCarWith2DoorsUnloked.allDoorLocked());

}

Non seulement le nombre de lignes a fondu, mais chacune des lignes devient subitement beaucoup plus explicite. C'est un aspect très important des tests puisque ceux-ci doivent servir idéalement de documentation exécutable. Il est désormais possible de lire le test comme une petite histoire:

  1. Il était une fois une voiture avec deux portes déverrouillées...
  2. Quand, soudain, on demande à la petite voiture de se déverrouiller...
  3. Alors, toutes ses portes se déverrouillent...
  4. Ainsi, le petit chaperon peut embarquer rapidement dans sa voiture sans se faire mouiller par la pluie qui faisait rage par cette journée de printemps... Mais ça, le test ne le dit pas...

Remarquons qu'avec l'utilisation du Builder, la section de configuration (given) a un sens et n'est pas polluée par des bruits inutiles et sans effet pour ce test. Ainsi, la couleur de la voiture n'est pas précisée puisqu'elle n'influence en rien ce cas de test. Par contre, il demeure qu'il s'agit d'une information requise afin d'initialiser la voiture et cet argument doit, en conséquence, rester.

Ainsi, le Builder permet de cacher ce qui n'est pas directement un facteur impliqué dans le cas de test en proposant initialement un modèle "standard" qui sert de base. Ensuite, le Builder offre des "options", à la pièce, pour personnaliser notre voiture afin qu'elle soit dans l'état nécessaire pour un test particulier. Cette configuration sous la forme d'options rend explicite les particularités et l'état initial de notre test tout en ne montrant que l'essentiel pour ce test.

Finalement, notons la disparition des commentaires qui permettaient de séparer les sections du test. Ceux-ci ne sont plus nécessaires, car le Builder permet l'écriture de la section Given en bloc.

blog comments powered by Disqus

0 Comments:

Post a comment

Comments have been closed for this post.