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
- Comment écrire des tests unitaires clairs et lisibles avec JUnit et les frameworks d’assertion
- Comment éviter les tests positifs faux dans certains cas
- Ce qu’il faut éviter lors de l’écriture de tests unitaires
É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:
@Test
public void getMessage() {
assertThat(service).isNotNull();
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
Ce test produit des erreurs comme celle-ci:
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
estnull
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.
@Test
public void getMessage() {
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
Le test modifié produit une erreur comme celle-ci:
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:
// #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.
#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:
// #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.
#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:
- Compter les pays trouvés.
- Vérifier la première entrée avec plusieurs valeurs.
De tels tests peuvent ressembler à cet exemple:
@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.
@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.
@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:
@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.
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
etINFO
@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
@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