Tutoriel JUnit 5, partie 1: Tests unitaires avec JUnit 5, Mockito et Hamcrest

JUnit 5 est le nouveau standard de facto pour le développement de tests unitaires en Java. Cette dernière version a laissé derrière elle les contraintes de Java 5 et a intégré de nombreuses fonctionnalités de Java 8, notamment la prise en charge des expressions lambda.

Dans cette première moitié d'une introduction en deux parties à JUnit 5, vous allez commencer à tester avec JUnit 5. Je vais vous montrer comment configurer un projet Maven pour utiliser JUnit 5, comment écrire des tests à l'aide des annotations @Testet @ParameterizedTest, et comment travailler avec les nouvelles annotations de cycle de vie dans JUnit 5. Vous verrez également un bref exemple d'utilisation des balises de filtre, et je vais vous montrer comment intégrer JUnit 5 avec une bibliothèque d'assertions tierce - dans ce cas, Hamcrest . Enfin, vous obtiendrez un tutoriel rapide sur l'intégration de JUnit 5 avec Mockito, afin que vous puissiez écrire des tests unitaires plus robustes pour des systèmes complexes et réels.

télécharger Obtenez le code Obtenez le code source pour des exemples dans ce didacticiel. Créé par Steven Haines pour JavaWorld.

Développement piloté par les tests

Si vous développez du code Java depuis un certain temps, vous êtes probablement intimement familiarisé avec le développement piloté par les tests, je vais donc garder cette section brève. Cependant, il est important de comprendre pourquoi nous écrivons des tests unitaires, ainsi que les stratégies employées par les développeurs lors de la conception des tests unitaires.

Le développement piloté par les tests (TDD) est un processus de développement logiciel qui associe le codage, les tests et la conception. Il s'agit d'une approche test-first qui vise à améliorer la qualité de vos applications. Le développement piloté par les tests est défini par le cycle de vie suivant:

  1. Ajoutez un test.
  2. Exécutez tous vos tests et observez l'échec du nouveau test.
  3. Implémentez le code.
  4. Exécutez tous vos tests et observez le succès du nouveau test.
  5. Refactorisez le code.

La figure 1 montre ce cycle de vie TDD.

Steven Haines

Il y a un double objectif à écrire des tests avant d'écrire votre code. Premièrement, cela vous oblige à réfléchir au problème commercial que vous essayez de résoudre. Par exemple, comment les scénarios réussis devraient-ils se comporter? Quelles conditions devraient échouer? Comment devraient-ils échouer? Deuxièmement, tester d'abord vous donne plus de confiance dans vos tests. Chaque fois que j'écris des tests après avoir écrit du code, je dois toujours les casser pour m'assurer qu'ils détectent réellement des erreurs. L'écriture des tests évite d'abord cette étape supplémentaire.

Ecrire des tests pour le chemin heureux est généralement facile: avec une bonne entrée, la classe doit renvoyer une réponse déterministe. Mais l'écriture de cas de test négatifs (ou d'échec), en particulier pour les composants complexes, peut être plus compliquée.

À titre d'exemple, envisagez d'écrire des tests pour un référentiel de base de données. Sur le chemin heureux, nous insérons un enregistrement dans la base de données et recevons en retour l'objet créé, y compris les clés générées. En réalité, nous devons également considérer la possibilité d'un conflit, tel que l'insertion d'un enregistrement avec une valeur de colonne unique qui est déjà détenue par un autre enregistrement. De plus, que se passe-t-il lorsque le référentiel ne peut pas se connecter à la base de données, peut-être parce que le nom d'utilisateur ou le mot de passe a changé? Que se passe-t-il s'il y a une erreur réseau en transit? Que se passe-t-il si la demande ne se termine pas dans le délai d'expiration défini?

Pour créer un composant robuste, vous devez prendre en compte tous les scénarios probables et improbables, développer des tests pour eux et écrire votre code pour satisfaire ces tests. Plus loin dans l'article, nous examinerons les stratégies de création de différents scénarios de défaillance, ainsi que certaines des nouvelles fonctionnalités de JUnit 5 qui peuvent vous aider à tester ces scénarios.

Adopter JUnit 5

Si vous utilisez JUnit depuis un certain temps, certaines des modifications apportées à JUnit 5 seront un ajustement. Voici un résumé de haut niveau de ce qui est différent entre les deux versions:

  • JUnit 5 est maintenant intégré au org.junit.jupitergroupe, ce qui change la façon dont vous l'incluerez dans vos projets Maven et Gradle.
  • JUnit 4 nécessitait un JDK minimum de JDK 5; JUnit 5 nécessite un minimum de JDK 8.
  • JUnit 4 de @Before, @BeforeClass, @After, et @AfterClassannotations ont été remplacées par @BeforeEach, @BeforeAll, @AfterEachet @AfterAll, respectivement.
  • L' @Ignoreannotation de JUnit 4 a été remplacée par l' @Disabledannotation.
  • L' @Categoryannotation a été remplacée par l' @Tagannotation.
  • JUnit 5 ajoute un nouvel ensemble de méthodes d'assertion.
  • Les coureurs ont été remplacés par des extensions, avec une nouvelle API pour les implémenteurs d'extensions.
  • JUnit 5 introduit des hypothèses qui empêchent l'exécution d'un test.
  • JUnit 5 prend en charge les classes de test imbriquées et dynamiques.

Nous explorerons la plupart de ces nouvelles fonctionnalités dans cet article.

Test unitaire avec JUnit 5

Commençons simplement, avec un exemple de bout en bout de configuration d'un projet pour utiliser JUnit 5 pour un test unitaire. Le listing 1 montre une MathToolsclasse dont la méthode convertit un numérateur et un dénominateur en a double.

Listing 1. Un exemple de projet JUnit 5 (MathTools.java)

 package com.javaworld.geekcap.math; public class MathTools { public static double convertToDecimal(int numerator, int denominator) { if (denominator == 0) { throw new IllegalArgumentException("Denominator must not be 0"); } return (double)numerator / (double)denominator; } }

Nous avons deux scénarios principaux pour tester la MathToolsclasse et sa méthode:

  • Un test valide , dans lequel nous passons des entiers non nuls pour le numérateur et le dénominateur.
  • Un scénario d'échec , dans lequel nous passons une valeur nulle pour le dénominateur.

Le listing 2 montre une classe de test JUnit 5 pour tester ces deux scénarios.

Listing 2. Une classe de test JUnit 5 (MathToolsTest.java)

 package com.javaworld.geekcap.math; import java.lang.IllegalArgumentException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class MathToolsTest { @Test void testConvertToDecimalSuccess() { double result = MathTools.convertToDecimal(3, 4); Assertions.assertEquals(0.75, result); } @Test void testConvertToDecimalInvalidDenominator() { Assertions.assertThrows(IllegalArgumentException.class, () -> MathTools.convertToDecimal(3, 0)); } }

Dans le listing 2, la testConvertToDecimalInvalidDenominatorméthode exécute la MathTools::convertToDecimalméthode à l'intérieur d'un assertThrowsappel. Le premier argument est le type d'exception attendu. Le deuxième argument est une fonction qui lèvera cette exception. La assertThrowsméthode exécute la fonction et valide que le type d'exception attendu est levé.

La classe Assertions et ses méthodes

L'  org.junit.jupiter.api.Testannotation désigne une méthode de test. Notez que l' @Testannotation provient désormais du package de l'API JUnit 5 Jupiter au lieu du package de JUnit 4 org.junit. La testConvertToDecimalSuccessméthode exécute d'abord la MathTools::convertToDecimalméthode avec un numérateur de 3 et un dénominateur de 4, puis affirme que le résultat est égal à 0,75. La org.junit.jupiter.api.Assertionsclasse fournit un ensemble de staticméthodes pour comparer les résultats réels et attendus. La Assertionsclasse a les méthodes suivantes, qui couvrent la plupart des types de données primitifs:

  • assertArrayEquals compare le contenu d'un tableau réel à un tableau attendu.
  • assertEquals compare une valeur réelle à une valeur attendue.
  • assertNotEquals compare deux valeurs pour valider qu'elles ne sont pas égales.
  • assertTrue valide que la valeur fournie est vraie.
  • assertFalse valide que la valeur fournie est fausse.
  • assertLinesMatchcompare deux listes de l' Stringart.
  • assertNull validates that the provided value is null.
  • assertNotNull validates that the provided value is not null.
  • assertSame validates that two values reference the same object.
  • assertNotSame validates that two values do not reference the same object.
  • assertThrows validates that the execution of a method throws an expected exception (you can see this in the testConvertToDecimalInvalidDenominator example above).
  • assertTimeout validates that a supplied function completes within a specified timeout.
  • assertTimeoutPreemptively validates that a supplied function completes within a specified timeout, but once the timeout is reached it kills the function's execution.

If any of these assertion methods fail, the unit test is marked as failed. That failure notice will be written to the screen when you run the test, then saved in a report file.

Using delta with assertEquals

When using float and double values in an assertEquals, you can also specify a delta that represents a threshold of difference between the two. In our example we could have added a delta of 0.001, in case 0.75 was actually returned as 0.750001.

Analyzing your test results

In addition to validating a value or behavior, the assert methods can also accept a textual description of the error, which can help you diagnose failures. For example:

 Assertions.assertEquals(0.75, result, "The MathTools::convertToDecimal value did not return the correct value of 0.75 for 3/4"); Assertions.assertEquals(0.75, result, () -> "The MathTools::convertToDecimal value did not return the correct value of 0.75 for 3/4"); 

The output will show the expected value of 0.75 and the actual value. It will also display the specified message, which can help you understand the context of the error. The difference between the two variations is that the first one always creates the message, even if it is not displayed, whereas the second one only constructs the message if the assertion fails. In this case, the construction of the message is trivial, so it doesn't really matter. Still, there is no need to construct an error message for a test that passes, so it's usually a best practice to use the second style.

Finally, if you're using an IDE like IntelliJ to run your tests, each test method will be displayed by its method name. This is fine if your method names are readable, but you can also add a @DisplayName annotation to your test methods to better identify the tests:

@Test @DisplayName("Test successful decimal conversion") void testConvertToDecimalSuccess() { double result = MathTools.convertToDecimal(3, 4); Assertions.assertEquals(0.751, result); }

Running your unit test

In order to run JUnit 5 tests from a Maven project, you need to include the maven-surefire-plugin in the Maven pom.xml file and add a new dependency. Listing 3 shows the pom.xml file for this project.

Listing 3. Maven pom.xml for an example JUnit 5 project

  4.0.0 com.javaworld.geekcap junit5 jar 1.0-SNAPSHOT    org.apache.maven.plugins maven-compiler-plugin 3.8.1  8 8    org.apache.maven.plugins maven-surefire-plugin 3.0.0-M4    junit5 //maven.apache.org   org.junit.jupiter junit-jupiter 5.6.0 test   

JUnit 5 dependencies

JUnit 5 packages its components in the org.junit.jupiter group and we need to add the junit-jupiter artifact, which is an aggregator artifact that imports the following dependencies:

  • junit-jupiter-api defines the API for writing tests and extensions.
  • junit-jupiter-engine est l'implémentation du moteur de test qui exécute les tests unitaires.
  • junit-jupiter-params fournit un support pour les tests paramétrés.

Ensuite, nous devons ajouter le maven-surefire-pluginplug-in de construction afin d'exécuter les tests.

Enfin, assurez-vous d'inclure le maven-compiler-pluginavec une version de Java 8 ou une version ultérieure, afin de pouvoir utiliser les fonctionnalités de Java 8 telles que lambdas.

Exécuter!

Utilisez la commande suivante pour exécuter la classe de test à partir de votre IDE ou de Maven:

mvn clean test

Si vous réussissez, vous devriez voir une sortie similaire à ce qui suit:

 [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.javaworld.geekcap.math.MathToolsTest [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.04 s - in com.javaworld.geekcap.math.MathToolsTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.832 s [INFO] Finished at: 2020-02-16T08:21:15-05:00 [INFO] ------------------------------------------------------------------------