Des matchers personnalisés et vos tests gagneront en lisibilité

Jean-Baptiste Le Duigou
3 min readJun 12, 2019

--

Photo by Sherman Yang on Unsplash

Dans un précédent billet je vous parlais de l’utilité de choisir les bonnes assertions dans vos tests. En fin d’article j’évoquais la possibilité d’implémenter ses propres matchers. C’est ce dont on va parler aujourd’hui !

Ecrire ses propres matchers apporte à mon avis deux avantages :

  • Améliorer les messages d’erreurs lorsque les tests échouent
  • Augmenter la lisibilité du code des tests

Voyons cela tout de suite avec un exemple.
Nous allons tester la classe BeerService et plus particulièrement la méthode getBeerById() :

Pour rappel la classe Beer ressemble à ça :

Pour commencer nous allons implémenter notre matcher qui sera de type Matcher<Beer>.
Il serait tentant d’implémenter directement l’interface Matcher, mais un petit tour par la Javadoc de cette interface devrait suffire à nous convaincre du contraire.

Matcher implementations should NOT directly implement this interface. Instead, extend the BaseMatcher abstract class, which will ensure that the Matcher API can grow to support new features and remain compatible with all Matcher implementations.

La solution logique serait maintenant d’hériter de la classe BaseMatcher, mais il y a une autre classe dont il peut-être intéressant d’hériter : TypeSafeMatcher. L’avantage est qu’elle vérifie la non nullité de l’élément testé, le type de cet objet et effectue une conversion de type.
Il va falloir implémenter les deux méthodes abstraites matchesSafely() et describeTo().
Ce qui nous donne :

Notez bien qu’il n’est pas nécessaire de tester si beer est null dans matchesSafely, ce test est déjà effectué par TypeSafeMatcher :

/**
* Methods made final to prevent accidental override.
* If you need to override this, there's no point on extending TypeSafeMatcher.
* Instead, extend the {
@link BaseMatcher}.
*/
@Override
@SuppressWarnings({"unchecked"})
public final boolean matches(Object item) {
return item != null
&& expectedType.isInstance(item)
&& matchesSafely((T) item);
}

Afin de faciliter l’écriture des tests nous allons tout de suite implémenter une factory :

Ecrivons un test qui échoue afin de vérifier le comportement de ce matcher :

Nous avons déjà un comportement correct puisque l’on comprend aisément ce qui est attendu et ce que l’on a réellement obtenu :

java.lang.AssertionError: 
Expected: beer with name "Punk IPA"
but: was <Beer(id=9531, name=Nanny State, alcoholByVolume=0.5)>

Il malgré tout possible d’améliorer cette dernière ligne en surchargeant la méthode describeMismatchSafely() :

Si l’on relance le test nous avons maintenant le message suivant :

java.lang.AssertionError: 
Expected: beer with name "Punk IPA"
but: name was "Nanny State"

Nous pouvons maintenant implémenter un deuxième matcher :

Il faut mettre à jour la factory :

Il est maintenant possible de combiner les deux matchers dans la même assertion :

Je trouve qu’avec ces deux matchers le code du test est très lisible et nous l’avons vu les messages d’erreurs sont également très explicites en cas d’erreur.

Le code des exemples utilisé dans cet article est disponible sur GitHub : https://github.com/jbleduigou/beer-api-java

N’hésitez pas à me faire part de vos commentaires ou questions en bas de cet article ou en m’envoyant un message sur LinkedIn :
http://www.linkedin.com/in/jbleduigou/en

--

--