Indices pour les tests unitaires avec AssertJ

Tests unitaires sont devenus une partie standard du développement. De nombreux outils peuvent être utilisés de différentes manières pour cela. Cet article démontre quelques conseils ou, pour le dire autrement, des bonnes pratiques qui ont bien fonctionné pour moi.

Dans cet article, vous apprendrez

Évitez l’excès de vérification de NPE

Nous avons tous tendance à éviter le NullPointerException autant que possible dans le code principal car cela peut entraîner des conséquences désagréables. Je pense que notre principal souci n’est pas d’éviter les NPE dans les tests. Notre objectif est de vérifier le comportement du composant testé d’une manière claire, lisible et fiable.

Mauvaise Pratique

À de nombreuses reprises par le passé, j’ai utilisé l’assertion isNotNull même lorsque cela n’était pas nécessaire, comme dans l’exemple ci-dessous:

Java

 

@Test
public void getMessage() {
	assertThat(service).isNotNull();
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}

Ce test produit des erreurs comme celle-ci:

Plain Text

 

java.lang.AssertionError: 
Expecting actual not to be null
	at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)

Bonne Pratique

Même si l’assertion supplémentaire isNotNull n’est pas vraiment nuisible, elle devrait être évitée pour les raisons suivantes:

  • Elle n’ajoute aucune valeur supplémentaire. C’est simplement plus de code à lire et à maintenir.
  • Le test échoue de toute façon lorsque service est null et nous voyons la véritable cause racine de l’échec. Le test remplit toujours son objectif.
  • Le message d’erreur produit est encore meilleur avec l’assertion AssertJ.

Voir l’assertion de test modifiée ci-dessous.

Java

 

@Test
public void getMessage() {
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}

Le test modifié produit une erreur comme celle-ci:

Java

 

java.lang.NullPointerException: Cannot invoke "com.github.aha.poc.junit.spring.HelloService.getMessage()" because "this.service" is null
	at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)

Remarque : L’exemple peut être trouvé dans SimpleSpringTest.

Affirmer les valeurs et non le résultat

De temps en temps, nous écrivons un test correct, mais d’une manière « mauvaise ». Cela signifie que le test fonctionne exactement comme prévu et vérifie notre composant, mais l’échec ne fournit pas assez d’informations. Par conséquent, notre objectif est d’affirmer la valeur et non le résultat de la comparaison.

Mauvaise Pratique

Examinons quelques-uns de ces mauvais tests:

Java

 

// #1
assertThat(argument.contains("o")).isTrue();

// #2
var result = "Welcome to JDK 10";
assertThat(result instanceof String).isTrue();

// #3
assertThat("".isBlank()).isTrue();

// #4
Optional<Method> testMethod = testInfo.getTestMethod();
assertThat(testMethod.isPresent()).isTrue();

Quelques erreurs des tests ci-dessus sont présentées ci-dessous.

Plain Text

 

#1
Expecting value to be true but was false
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
	at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
  
#3
Expecting value to be true but was false
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
	at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)

Bonne Pratique

La solution est assez simple avec AssertJ et son API fluide. Tous les cas mentionnés ci-dessus peuvent être facilement réécrits comme suit:

Java

 

// #1
assertThat(argument).contains("o");

// #2
assertThat(result).isInstanceOf(String.class);

// #3
assertThat("").isBlank();

// #4
assertThat(testMethod).isPresent();

Les mêmes erreurs mentionnées précédemment apportent maintenant plus de valeur.

Plain Text

 

#1
Expecting actual:
  "Hello"
to contain:
  "f" 
	at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
    
#3
Expecting blank but was: "a"
	at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)

Note: L’exemple peut être trouvé dans SimpleParamTests.

Rassembler les Assertions Liées

La chaînage des assertions et l’indentation du code associé contribuent grandement à la clarté et à la lisibilité des tests.

Mauvaise Pratique

Lorsque nous écrivons un test, nous pouvons aboutir à un test correct, mais moins lisible. Imaginons un test où nous voulons rechercher des pays et effectuer ces vérifications:

  1. Compter les pays trouvés. 
  2. Vérifier la première entrée avec plusieurs valeurs.

De tels tests peuvent ressembler à cet exemple:

Java

 

@Test
void listCountries() {
	List<Country> result = ...;

	assertThat(result).hasSize(5);
	var country = result.get(0);
	assertThat(country.getName()).isEqualTo("Spain");
	assertThat(country.getCities().stream().map(City::getName)).contains("Barcelona");
}

Bonne Pratique

Même si le test précédent est correct, nous devrions améliorer beaucoup la lisibilité en regroupant les assertions liées ensemble (lignes 9-11). L’objectif ici est d’affirmer result une fois et d’écrire autant d’assertions chaînées que nécessaire. Voir la version modifiée ci-dessous.

Java

 

@Test
void listCountries() {
	List<Country> result = ...;

	assertThat(result)
		.hasSize(5)
		.singleElement()
		.satisfies(c -> {
			assertThat(c.getName()).isEqualTo("Spain");
			assertThat(c.getCities().stream().map(City::getName)).contains("Barcelona");
		});
}

Note: L’exemple peut être trouvé dans CountryRepositoryOtherTests.

Prévenir un Test Réussi Faux Positif

Lorsqu’une méthode d’assertion utilisant l’argument ThrowingConsumer est employée, l’argument doit également contenir assertThat dans le consommateur. Sinon, le test passerait constamment – même lorsque la comparaison échoue, ce qui signifie un test incorrect. Le test ne réussit que lorsqu’une assertion lance une exception RuntimeException ou AssertionError. Je suppose que c’est clair, mais il est facile d’oublier cela et d’écrire un test incorrect. Cela m’arrive de temps en temps.

Mauvaise Pratique

Imaginons que nous ayons un ensemble de codes de pays et que nous voulions vérifier que chaque code satisfait une certaine condition. Dans notre cas fictif, nous voulons affirmer que chaque code de pays contient le caractère « a ». Comme vous pouvez le voir, c’est absurde : nous avons des codes en majuscules, mais nous n’appliquons pas l’insensibilité à la casse dans l’assertion.

Java

 

@Test
void assertValues() throws Exception {
	var countryCodes = List.of("CZ", "AT", "CA");
	
	assertThat( countryCodes )
		.hasSize(3)
		.allSatisfy(countryCode -> countryCode.contains("a"));
}

Étonnamment, notre test a réussi avec succès.

Bonne Pratique

Comme mentionné au début de cette section, notre test peut être corrigé facilement avec une assertThat supplémentaire dans le consommateur (ligne 7). Le test correct devrait être le suivant:

Java

 

@Test
void assertValues() throws Exception {
	var countryCodes = List.of("CZ", "AT", "CA");
	
	assertThat( countryCodes )
		.hasSize(3)
		.allSatisfy(countryCode -> assertThat( countryCode ).containsIgnoringCase("a"));
}

Maintenant, le test échoue comme prévu avec le message d’erreur correct.

Plain Text

 

java.lang.AssertionError: 
Expecting all elements of:
  ["CZ", "AT", "CA"]
to satisfy given requirements, but these elements did not:

"CZ"
error: 
Expecting actual:
  "CZ"
to contain:
  "a"
 (ignoring case)
	at com.github.aha.sat.core.clr.AppleTest.assertValues(AppleTest.java:45)

Chaînage des Assertions

Le dernier conseil n’est pas vraiment une pratique, mais plutôt une recommandation. L’API fluide d’AssertJ devrait être utilisée afin de créer des tests plus lisibles.

Assertions Non Chaînées

Considérons le test listLogs, dont le but est de tester l’enregistrement d’un composant. L’objectif ici est de vérifier :

  • Nombre affirmé de journaux collectés
  • Affirmer l’existence des messages de journal DEBUG et INFO
Java

 

@Test
void listLogs() throws Exception {
	ListAppender<ILoggingEvent> logAppender = ...;
	
	assertThat( logAppender.list ).hasSize(2);
	assertThat( logAppender.list ).anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
			assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
		});
	assertThat( logAppender.list ).anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(INFO);
			assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
		});
}

Chaînage des Assertions

Avec l’API fluide mentionnée et le chaînage, nous pouvons modifier le test de cette manière

Java

 

@Test
void listLogs() throws Exception {
	ListAppender<ILoggingEvent> logAppender = ...;
	
	assertThat( logAppender.list )
		.hasSize(2)
		.anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
			assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
		})
		.anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(INFO);
			assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
		});
}

Note : l’exemple peut être trouvé dans AppleTest.

Résumé et Code Source

Le framework AssertJ fournit beaucoup d’aide avec leur API fluide. Dans cet article, plusieurs conseils et astuces ont été présentés afin de produire des tests plus clairs et plus fiables. S’il vous plaît, soyez conscient que la plupart de ces recommandations sont subjectives. Cela dépend des préférences personnelles et du style de code.

Le code source utilisé peut être trouvé dans mes dépôts

Source:
https://dzone.com/articles/hints-for-unit-testing-with-assertj