Vous avez réussi à coder votre premier test unitaire JUnit et à comprendre la démarche TDD. Pour donner un maximum d'informations à vos classes et à vos méthodes de test JUnit, vous allez utiliser les annotations Java.
Une annotation Java, c’est un mot clé précédé du symbole “arobase”, que l’on place juste au-dessus d’un élément en Java : un nom de classe, de méthode, ou même de paramètre de méthode. Elle permet de donner une information précise pour décrire cet élément. Et cette information peut être utilisée pour modifier la manière dont votre code va être exécuté. Dans le cas des tests, le framework JUnit 5 va se servir des annotations pour savoir comment lancer les tests. Par exemple, dans le chapitre précédent, on a placé l’annotation @Test au-dessus de chaque méthode qui devait être lancée comme un test.
Grâce aux annotations, vous n'avez pas à coder le lancement des tests ou d'autres fonctionnalités propres aux tests. Vous avez simplement à ajouter des annotations, et JUnit s'occupe de tout ! C'est très pratique !
JUnit offre de nombreuses annotations utiles, et je vais vous en présenter quelques-unes. Elles peuvent vous aider à rendre vos tests plus pertinents et vous économiser des lignes de code ! Pour utiliser ces annotations, il suffit de les ajouter juste avant la ligne qui déclare votre classe de test ou votre méthode. Sur l'extrait de code ci-dessous, tous les mots clés précédés d'un @ sont des annotations JUnit 5.
public class MaClasseDeTest {
@BeforeEach
public void methodeAppeleeAvantChaqueTest() {
...
}
@AfterEach
public void methodeAppeleeApresChaqueTest() {
...
}
@BeforeAll
public static void methodeAppeleeAvantTousLesTests() {
...
}
@AfterAll
public static void methodeAppeleeApresTousLesTests() {
...
}
@Test
public void unTest() {
// Arrange
...
// Act
...
// Assert
...
}
}
Découvrez les annotations principales de JUnit
Reprenons notre classe CalculatorTest. Étant donné que nous testons un calculateur, améliorons pas à pas CalculatorTest, en utilisant les annotations basiques suivantes : @BeforeAll, @AfterAll, @BeforeEach, @AfterEach, puis d'autres plus sophistiquées comme @ParameterizedTest et @Timeout.
Pour partir sur la même base de code que ce cours, je vous conseille vivement de cloner le dépôt GitHub associé à ce cours :
git clone https://github.com/geoffreyarthaud/oc-testing-java-cours.git
Puis placez-vous dans la branche de ce chapitre :
git checkout p1ch3
Ensuite, importez le projet en tant que projet Maven. C'est parti pour un nouveau screencast ! Reportez-vous au chapitre précédent pour connaître les outils que j'utilise dans les screencasts.
Voyons de plus près les étapes principales de ce screencast :
les annotations pour faire une action avant ou après vos tests ;
les annotations pour rendre paramétrables les tests ;
et la gestion du temps de traitement.
Effectuez des actions avant ou après vos tests grâce aux annotations
Pour notre exemple de calculateur, nous allons nous fixer les objectifs suivants pour nos tests :
Avant chaque test, initialiser une instance du calculateur.
Après chaque test, mettre la valeur du calculateur à null.
Mesurer le temps de traitement de l'ensemble des tests de CalculatorTest.
Tout d'abord, stockons l'instance en cours du calculateur et une variable de temps du début des tests (ce sera pour l'objectif 3) dans notre classe de tests :
public class CalculatorTest {
private static Instant startedAt;
private Calculator calculatorUnderTest;
...
Ensuite, pour répondre à l'objectif 1 (initialiser une instance du calculateur avant chaque test), créons une méthode pour initialiser le calculateur (et ajouter un message à la console). Il faut rajouter l'annotation @BeforeEach. Cela donne :
@BeforeEach
public void initCalculator() {
System.out.println("Appel avant chaque test");
calculatorUnderTest = new Calculator();
}
Et c'est tout ! Ou presque. Il faut à présent supprimer les initialisations de Calculator dans les méthodes de tests pour utiliser l'instance calculatorUnderTest. Avant, on avait ce code :
public void testAddTwoPositiveNumbers() {
// Arrange
int a = 2;
int b = 3;
Calculator calculator = new Calculator();
// Act
int somme = calculator.add(a, b);
...
On obtient, après remplacement, les deux tests suivants :
@Test
public void testAddTwoPositiveNumbers() {
// Arrange
int a = 2;
int b = 3;
// Act
int somme = calculatorUnderTest.add(a, b);
// Assert
assertEquals(5, somme);
}
@Test
public void multiply_shouldReturnTheProduct_ofTwoIntegers() {
// Arrange
int a = 42;
int b = 11;
// Act
int produit = calculatorUnderTest.multiply(a, b);
// Assert
assertEquals(462, produit);
}
Ensuite, pour l'objectif 2 (après chaque test, mettre la valeur du calculateur à null), nous allons utiliser l'annotation @AfterEach :
@AfterEach
public void undefCalculator() {
System.out.println("Appel après chaque test");
calculatorUnderTest = null;
}
Enfin, pour l'objectif 3 (mesurer le temps de traitement de l'ensemble des tests de CalculatorTest), nous allons créer des méthodes statiques annotées avec @BeforeAll et @AfterAll :
@BeforeAll
static public void initStartingTime() {
System.out.println("Appel avant tous les tests");
startedAt = Instant.now();
}
@AfterAll
static public void showTestDuration() {
System.out.println("Appel après tous les tests");
Instant endedAt = Instant.now();
long duration = Duration.between(startedAt, endedAt).toMillis();
System.out.println(MessageFormat.format("Durée des tests : {0} ms", duration));
}
Grâce à ces quatre annotations, ce qui est intéressant, c'est que JUnit ne vous impose pas d'effectuer ces actions dans des méthodes avec un nommage spécifique comme setUp
ou tearDown()
(c'était le cas avec les anciennes versions de JUnit). Donc, profitez-en, soyez descriptif dans vos noms de méthodes !
Pourquoi ces méthodes et la variables startedAt sont-elles statiques ?
C'est un héritage des anciennes versions de JUnit. Les méthodes appelées avant ou après tous les tests sont donc statiques car considérées comme liées à l'objet de la classe de test, et non à une instance particulière.
Jouez avec les entrants et les sortants grâce aux tests paramétrés
Vous avez déjà vu l’annotation @Test. Imaginez que vous souhaitiez effectuer le même traitement de tests (l'étape Act), sur des entrants différents (de l'étape Arrange), afin de vérifier différents cas de figure.
JUnit 5 vous simplifie la vie ! Grâce à l'annotation @ParameterizedTest, à la place de @Test. Voyons cela de plus près.
Une première possibilité consiste à fournir plusieurs entrants, et le résultat attendu doit être le même pour tous. Par exemple, toute multiplication par zéro doit donner nécessairement zéro ! Nous allons fournir les différents entrants possibles avec l'annotation @ValueSource. Cette annotation accepte tous les types primitifs Java standard comme les valeurs ints, longs, strings, etc.
Ensuite, la méthode de test elle-même est dotée d'un argument. Cela donne le résultat ci-dessous.
@ParameterizedTest(name = "{0} x 0 doit être égal à 0")
@ValueSource(ints = { 1, 2, 42, 1011, 5089 })
public void multiply_shouldReturnZero_ofZeroWithMultipleIntegers(int arg) {
// Arrange -- Tout est prêt !
// Act -- Multiplier par zéro
int actualResult = calculatorUnderTest.multiply(arg, 0);
// Assert -- ça vaut toujours zéro !
assertEquals(0, actualResult);
}
Sympathique, non ? Et avez-vous remarqué que l'annotation @ParametrizedTest accepte un paramètre pour formater le nom du test en fonction du paramètre ?
Mais, à quoi cela sert ?
Eh bien, cela améliore l'affichage du résultat dans JUnit. Sans ce paramètre, voici ce que cela donne :
Et avec un paramètre de formatage :
Bon, une liste d'entrants, c'est pas mal, mais vous aimeriez peut-être que votre test ait plusieurs paramètres ? Par exemple, pour tester l'addition, fournir une liste d'éléments contenant chacun deux nombres entrants et la somme attendue de ces deux nombres ?
JUnit 5 a une annotation pour cela. Vous pouvez utiliser @CsvSourse à la place de @ValueSource. Voici un exemple d'utilisation :
@ParameterizedTest(name = "{0} + {1} should equal to {2}")
@CsvSource({ "1,1,2", "2,3,5", "42,57,99" })
public void add_shouldReturnTheSum_ofMultipleIntegers(int arg1, int arg2, int expectResult) {
// Arrange -- Tout est prêt !
// Act
int actualResult = calculatorUnderTest.add(arg1, arg2);
// Assert
assertEquals(expectResult, actualResult);
}
La liste d'entrants/sortants est formatée sous forme de chaînes de caractères, et chaque chaîne possède un jeu de paramètres, séparé par des virgules. Dans l'exemple ci-dessus, les triplets de valeur :
1, 1 et 2
2, 3 et 5
42, 57 et 99
représentent chacun un jeu de paramètres. Et comme vous pouvez le voir, chacun de ces jeux de paramètres est utilisable pour la méthode de test, mais aussi pour le formatage du nom du test affiché dans les résultats JUnit :
Testez la vitesse de vos traitements
Certaines fonctionnalités peuvent prendre du temps à être traitées. Si vous souhaitez vérifier que ce délai ne soit pas trop long, vous pouvez décider de faire échouer le test à partir d'un délai que vous estimez trop long.
L'annotation @Timeout est fait pour ça. Elle prend en argument le délai à partir duquel vous souhaitez faire échouer le test (en secondes par défaut) :
@Timeout(1)
@Test
public void longCalcul_shouldComputeInLessThan1Second() {
// Arrange
// Act
calculatorUnderTest.longCalculation();
// Assert
// ...
}
Dans notre classe Calculator, la méthode longCalculation peut ressembler à cela :
public void longCalculation() {
try {
// Attendre 2 secondes
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Dans ce cas, votre test échouera au bout d'une seconde. Si vous remplacez la valeur 2 000 par 500 (en ms, c'est-à-dire une demi-seconde) dans longCalculation, votre test passera en succès au bout d'une demi-seconde !
En résumé
Les annotations JUnit vous aident à écrire des tests plus clairs sans répétitions inutiles.
Voici quelques annotations courantes :
Annotation | Quand l’utiliser |
@BeforeEach | Exécutez une méthode avant chaque test. C’est un très bon emplacement pour installer ou organiser un prérequis pour vos tests. |
@AfterEach | Exécutez une méthode après chaque test. C’est un très bon emplacement pour nettoyer ou satisfaire à une postcondition. |
@BeforeAll | Désignez une méthode statique pour qu’elle soit exécutée avant tous vos tests. Vous pouvez l’utiliser pour installer d’autres variables statiques pour vos tests. |
@AfterAll | Désignez une méthode statique pour qu’elle soit exécutée après tous vos tests. Vous pouvez utiliser ceci pour nettoyer les dépendances statiques. |
@ParametrizedTest | Vous souhaitez réutiliser le même test avec plusieurs entrants (@ValueSource) voire plusieurs entrants/sortants (@CsvSource). |
@Timeout | Si vous testez une méthode qui ne doit pas être trop lente, vous pouvez la forcer à échouer le test. |
Maintenant que vous avez annoté vos méthodes de tests, voyons de plus près la manière de coder l'étape Assert de vos tests, et en particulier les différents types d'assertions... dans le chapitre suivant !