• 10 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 11/12/2024

Structurez vos tests unitaires avec les annotations JUnit

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 :

  1. Avant chaque test, initialiser une instance du calculateur.

  2. Après chaque test, mettre la valeur du calculateur à null.

  3. 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 :

Résultat d'un @ParameterizedTest sans paramètre de formatage
Résultat d'un @ParameterizedTest sans paramètre de formatage

Et avec un paramètre de formatage :

Résultat d'un @ParameterizedTest avec paramètre de formatage
Résultat d'un @ParameterizedTest avec 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 :

Résultat d'un test paramétré avec plusieurs paramètres
Résultat d'un test paramétré avec jeux de paramètres

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 !

Exemple de certificat de réussite
Exemple de certificat de réussite