Vous avez réussi à coder vos premiers tests unitaires suivant une structuration JUnit 5. Nous allons à présent nous intéresser à l'objectif de vos tests et à ce qu'ils doivent vérifier. En particulier, vous allez découvrir comment mieux coder vos assertions. Pour rappel, l'assertion représente la dernière étape de vérification, qui est donc intimement liée à l'objectif de votre test unitaire.
Mais auparavant, comprenez en quoi le TDD et expliciter clairement l'objectif des tests sont importants pour votre application, et en particulier pour réduire son coût !
Utilisez le TDD pour éviter le coût des incompréhensions
Le développement piloté par les tests, ou TDD pour « test-driven development », vous aide à éviter ce problème. Comme vous l’avez vu précédemment, le TDD permet de créer un nouveau test automatique avant d’écrire du code. Pour éviter toute confusion sur ce que le logiciel est censé faire, il est essentiel que votre test explique clairement ce qui est testé. Ainsi, vous restez concentré sur la résolution d’un problème à la fois, et les autres membres de votre équipe comprennent mieux ce que vous essayez d’accomplir !
Pourquoi est-ce que je devrais procéder ainsi ?
Plusieurs études ont montré que plus un bug est découvert tardivement, plus il est coûteux de le corriger… le coût peut être multiplié jusqu’à des centaines de fois !
Le TDD vous permet de communiquer sur ce que vous testez, afin de détecter les bugs quand vous êtes en train de coder. Il peut vous éviter de coder les mauvaises choses, ou de mal comprendre un cas de test. Si vous découvrez que votre code est mauvais pendant que vous l’écrivez, vous pouvez le corriger immédiatement. Le TDD vous aide à faire cela en rendant vos cas de tests lisibles pour les autres. Cela vous permet également de gagner en confiance sur ce que votre code est censé réaliser !
S’il se passe un an avant que vous ne découvriez que votre application comporte des erreurs, il vous faudra peut-être partir à la chasse au bug à travers toute votre base de code ! Cela prendra beaucoup plus longtemps !
Mais comment je fais pour rendre mes tests suffisamment significatifs pour éviter les erreurs ?
Il existe des frameworks en complément de JUnit pour vous aider : Hamcrest (qui était inclus avec JUnit 4 mais séparé depuis JUnit 5), AssertJ et Truth. Ils sont tous là pour vous aider à affiner vos assertions, et rendre ainsi votre code de test plus lisible. Dans ce cours, nous allons utiliser AssertJ car il présente les avantages suivants :
il permet de créer un code lisible, en particulier les assertions, proches du langage naturel ;
il est simple à utiliser pour les développeurs.
Codez des tests faciles à lire avec AssertJ
Reprenons un test du chapitre précédent. Il utilisait assertEquals de JUnit.
@Test
public void testAddTwoPositiveNumbers() {
// Arrange
int a = 2;
int b = 3;
// Act
int somme = calculatorUnderTest.add(a, b);
// Assert
assertEquals(5, somme);
}
L'assertion à la ligne 11 est assez mathématique, normal pour un calculateur ! Mais dans la vie quotidienne, diriez-vous « assert equals », ou littéralement « affirmer l'égalité » ?
Vous : « Puis-je affirmer l'égalité entre bonne santé et ta santé ? »
Votre ami : ... 🤷🏻♀️
Les êtres humains ne parlent pas comme ça, même pas les développeurs ! 😛 Alors, pourquoi nos tests le feraient-ils ?
L’objectif est que vos tests expriment ce que doivent faire vos fonctionnalités. Les tests sont des outils pour communiquer avec d’autres développeurs, mais aussi avec vous-même dans quelques jours ou quelques mois. Si vous comprenez quel comportement est attendu, alors vous savez si ça fonctionne !
La bibliothèque populaire AssertJ , utilisée par de nombreux projets Java, permet de rendre les assertions de tests un peu plus naturelles à lire que les assertions de JUnit. Elle permet de chaîner plusieurs vérifications pour en faire une plus complexe. Voici quelques exemples :
Cas de test | Assertions JUnit | Assertions AssertJ |
Un nom est compris entre 5 et 10 caractères. | assertTrue(name.length > 4 && name.length < 11); | assertThat(name) .hasSizeGreaterThan(4) .hasSizeLessThan(11); |
Un nom est situé dans la première moitié de l'alphabet | assertTrue( name.compareTo("A") >= 0 && name.compareTo("M") <= 0); | assertThat(name).isBetween("A", "M"); |
Une date et heure locale se situent aujourd'hui ou dans le futur. | assertTrue( dateTime.toLocalDate().isAfter(LocalDate.now()) || dateTime.toLocalDate().isEqual(LocalDate.now())); | assertThat(dateTime.toLocalDate()) .isAfterOrEqualTo(LocalDate.now()); |
Vous voyez à travers ces exemples que les assertions de AssertJ sont plus riches et plus lisibles. En cas d'échec d'un test unitaire, comparé à assertTrue de JUnit, le message d'erreur sera plus lisible.
Ainsi, si l'on reprend la dernière assertion sur les dates, en cas d'échec du test, voici le message d'erreur avec JUnit :
org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
Et avec AssertJ :
java.lang.AssertionError:
Expecting:
<2019-08-29>
to be after or equal to:
<2019-08-30>
Vous voyez la différence ? AssertJ donne un résultat bien plus précis.
Utilisez AssertJ avec le calculateur
Êtes-vous prêt à essayer d’utiliser AssertJ ? Nous allons revoir les tests unitaires du calculateur en exploitant cette bibliothèque, et ajouter un nouveau test pour une nouvelle fonctionnalité du calculateur : lister l'ensemble des chiffres utilisés d'un nombre entier. En route pour un nouveau screencast !
Reprenons ensemble les étapes principales vues dans le screencast.
Installez AssertJ sous forme de dépendance Maven
Parcourez le site mvnrepository.com pour y trouver assertj-core. Copiez-collez la dépendance dans votre fichier pom.xml, sous la dépendance à JUnit :
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.5.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
À la sauvegarde de ce fichier, votre IDE s'occupera de télécharger et d'installer AssertJ sur votre projet.
Migrez vers AssertJ pour les tests d'addition et de multiplication
Tous les tests que nous avons effectués jusqu'à présent présentent des assertions simples : vérifier si le résultat était égal à un nombre. Donc, pour passer de JUnit pur à AssertJ, nous allons remplacer toutes les assertions :
assertEquals(expectedNumber, actualNumber)
par :
assertThat(actualNumber).isEqualTo(expectedNumber);
Par exemple, pour le test simple de l'addition :
assertThat(somme).isEqualTo(5);
La différence n'est pas énorme, je vous le concède. Mais ne trouvez-vous pas que c'est plus lisible ? Au début de mon utilisation de JUnit, je me posais toujours la question : dans assertEquals, quel élément mettre en premier ou deuxième argument, ce que l'on teste ou ce qui est attendu ? Au moins, avec AssertJ, on met toujours l'objet à tester (le fameux SUT) en argument de assertThat.
Voyons à présent un autre exemple d'utilisation de AssertJ, qui nécessiterait un code plus complexe et moins lisible en JUnit pur.
Utilisez AssertJ pour la nouvelle fonctionnalité à développer
Nous allons développer la fonctionnalité suivante : à partir d'un nombre entier, le calculateur donne l'ensemble des chiffres utilisés pour former ce nombre. L'ordre des chiffres indiqués n'est pas important.
En bon praticien du TDD, vous avez désormais compris que l'on va d'abord coder les tests : un test pour un nombre positif, un test pour un nombre négatif, et un test pour le nombre zéro.
Dans le screencast, vous avez pu voir que l'autocomplétion proposée par l'IDE est très pratique, et c'est l'une des forces principales de AssertJ. Dès que l'on a mis l'expression assertThat(monObjet), l'IDE propose les assertions adaptées en fonction du type d'objet.
@Test
public void digitsSet_shouldReturnsTheSetOfDigits_ofPositiveInteger() {
// GIVEN
int number = 95897;
// WHEN
Set<Integer> actualDigits = calculatorUnderTest.digitsSet(number);
// THEN
// assertThat(actualDigits).??? à compléter
}
Reste à savoir ce que l'on veut tester. D'après la fonctionnalité, on peut en déduire que l'on va vérifier si un ensemble non ordonné de nombres correspond exactement à la liste de chiffres à laquelle on doit s'attendre.
Parmi toutes les méthodes de AssertJ, certaines concernent les listes et les ensembles. Elles commencent souvent par contains[...]. Reste à savoir laquelle choisir. Si vous regardez bien, la méthode containsExactlyInAnyOrder paraît parfaitement adaptée : le même contenu, mais peu importe l'ordre. Voici donc le test complet avec cette méthode :
@Test
public void listDigits_shouldReturnsTheListOfDigits_ofPositiveInteger() {
// GIVEN
int number = 95897;
// WHEN
Set<Integer> actualDigits = calculatorUnderTest.digitsSet(number);
// THEN
assertThat(actualDigits).containsExactlyInAnyOrder(5, 7, 8, 9);
}
En JUnit pur, on doit reconstruire l'ensemble à comparer et utiliser à nouveau assertEquals, c'est moins sémantique :
@Test
public void listDigits_shouldReturnsTheListOfDigits_ofPositiveInteger() {
// GIVEN
int number = 95897;
// WHEN
Set<Integer> actualDigits = calculatorUnderTest.digitsSet(number);
// THEN
Set<Integer> expectedDigits = Stream.of(5, 7, 8, 9).collect(Collectors.toSet());
assertEquals(expectedDigits, actualDigits);
}
Après, soyons francs. Le langage, même avec AssertJ, est toujours un peu robotisé, n’est-ce pas ? Vous ne pouvez pas l’éviter car Java est un langage de programmation. Ce qui est important, c’est que vous ayez un langage fluide, qui puisse être utilisé pour décrire votre intention un peu plus naturellement.
Dans tous les cas, ce n’est que la partie émergée de l’iceberg. Parcourez la documentation de AssertJ pour découvrir toute l'API que vous pouvez utiliser. Et si vous aimez le Seigneur des Anneaux, les exemples utilisés dans cette documentation sont faits pour vous !
En résumé
En décrivant les tests avec un langage clair, vous pouvez détecter toute incompréhension rapidement.
Plus vous découvrez un défaut ou une incompréhension dans votre code tardivement, plus il sera coûteux de le corriger.
Les assertions de AssertJ fournissent des assertions plus lisibles. Vous pouvez les combiner de différentes manières pour exprimer clairement ce que vous testez.
Ces derniers chapitres vous ont permis de vous approprier les tests unitaires, l'esprit dans lequel il faut les coder et une première technique pour que vous soyez opérationnel. Avant d'approfondir sur des notions plus avancées de tests, nous allons aborder un autre thème : vous aider à vérifier que vous codez efficacement vos tests et que votre code d'application reste de qualité. Cela se passe... dans le prochain chapitre !