Un guide pour des tests unitaires et d'intégration robustes avec JUnit
Publié: 2022-03-11Les tests logiciels automatisés sont d'une importance cruciale pour la qualité, la maintenabilité et l'extensibilité à long terme des projets logiciels, et pour Java, JUnit est la voie vers l'automatisation.
Alors que la majeure partie de cet article se concentrera sur l'écriture de tests unitaires robustes et l'utilisation du stub, du mocking et de l'injection de dépendances, nous aborderons également les tests JUnit et d'intégration.
Le framework de test JUnit est un outil commun, gratuit et open source pour tester des projets basés sur Java.
Au moment d'écrire ces lignes, JUnit 4 est la version majeure actuelle, ayant été publiée il y a plus de 10 ans, la dernière mise à jour datant de plus de deux ans.
JUnit 5 (avec les modèles de programmation et d'extension Jupiter) est en cours de développement. Il prend mieux en charge les fonctionnalités de langage introduites dans Java 8 et inclut d'autres nouvelles fonctionnalités intéressantes. Certaines équipes peuvent trouver JUnit 5 prêt à l'emploi, tandis que d'autres peuvent continuer à utiliser JUnit 4 jusqu'à ce que 5 soit officiellement publié. Nous examinerons des exemples des deux.
Exécution de JUnit
Les tests JUnit peuvent être exécutés directement dans IntelliJ, mais ils peuvent également être exécutés dans d'autres IDE comme Eclipse, NetBeans ou même la ligne de commande.
Les tests doivent toujours être exécutés au moment de la construction, en particulier les tests unitaires. Une construction avec des tests défaillants doit être considérée comme ayant échoué, que le problème soit dans la production ou dans le code de test - cela nécessite de la discipline de la part de l'équipe et une volonté de donner la plus haute priorité à la résolution des tests défaillants, mais il est nécessaire d'adhérer à la esprit d'automatisation.
Les tests JUnit peuvent également être exécutés et signalés par des systèmes d'intégration continue tels que Jenkins. Les projets qui utilisent des outils tels que Gradle, Maven ou Ant ont l'avantage supplémentaire de pouvoir exécuter des tests dans le cadre du processus de construction.
Gradle
En tant qu'exemple de projet Gradle pour JUnit 5, consultez la section Gradle du guide de l'utilisateur JUnit et le référentiel junit5-samples.git. Notez qu'il peut également exécuter des tests utilisant l'API JUnit 4 (appelée "vintage" ).
Le projet peut être créé dans IntelliJ via l'option de menu Fichier > Ouvrir… > accédez au junit-gradle-consumer sub-directory
> OK > Ouvrir en tant que projet > OK pour importer le projet depuis Gradle.
Pour Eclipse, le plugin Buildship Gradle peut être installé depuis Help > Eclipse Marketplace… Le projet peut ensuite être importé avec File > Import… > Gradle > Gradle Project > Next > Next > Browse to the junit-gradle-consumer
sub-directory > Next > Suivant > Terminer.
Après avoir configuré le projet Gradle dans IntelliJ ou Eclipse, l'exécution de la tâche de build
Gradle comprendra l'exécution de tous les tests JUnit avec la tâche de test
. Notez que les tests peuvent être ignorés lors des exécutions ultérieures de la build
si aucune modification n'a été apportée au code.
Pour JUnit 4, voir Utilisation de JUnit avec Gradle wiki.
Maven
Pour JUnit 5, reportez-vous à la section Maven du guide de l'utilisateur et au référentiel junit5-samples.git pour un exemple de projet Maven. Cela peut également exécuter des tests vintage (ceux qui utilisent l'API JUnit 4).
Dans IntelliJ, utilisez Fichier > Ouvrir… > accédez à junit-maven-consumer/pom.xml
> OK > Ouvrir en tant que projet. Les tests peuvent ensuite être exécutés depuis Maven Projects > junit5-maven-consumer > Lifecycle > Test.
Dans Eclipse, utilisez Fichier > Importer… > Maven > Projets Maven existants > Suivant > Accédez au junit-maven-consumer
> Avec le pom.xml
sélectionné > Terminer.
Les tests peuvent être exécutés en exécutant le projet en tant que build Maven… > spécifier l'objectif du test
> Exécuter.
Pour JUnit 4, consultez JUnit dans le référentiel Maven.
Environnements de développement
En plus d'exécuter des tests via des outils de construction tels que Gradle ou Maven, de nombreux IDE peuvent exécuter directement des tests JUnit.
IDÉE IntelliJ
IntelliJ IDEA 2016.2 ou version ultérieure est requis pour les tests JUnit 5, tandis que les tests JUnit 4 devraient fonctionner dans les anciennes versions d'IntelliJ.
Pour les besoins de cet article, vous souhaiterez peut-être créer un nouveau projet dans IntelliJ à partir de l'un de mes référentiels GitHub ( JUnit5IntelliJ.git ou JUnit4IntelliJ.git), qui incluent tous les fichiers de l'exemple de classe Person
simple et utilisent le Bibliothèques JUnit. Le test peut être exécuté avec Exécuter > Exécuter 'Tous les tests'. Le test peut également être exécuté dans IntelliJ à partir de la classe PersonTest
.
Ces référentiels ont été créés avec de nouveaux projets Java IntelliJ et construisent les structures de répertoires src/main/java/com/example
et src/test/java/com/example
. Le répertoire src/main/java
a été spécifié comme dossier source tandis que src/test/java
a été spécifié comme dossier source de test. Après avoir créé la classe PersonTest
avec une méthode de test annotée avec @Test
, la compilation peut échouer, auquel cas IntelliJ propose la suggestion d'ajouter JUnit 4 ou JUnit 5 au chemin de classe qui peut être chargé à partir de la distribution IntelliJ IDEA (voir ces réponses sur Stack Overflow pour plus de détails). Enfin, une configuration d'exécution JUnit a été ajoutée pour tous les tests.
Voir également les directives pratiques sur les tests IntelliJ.
Éclipse
Un projet Java vide dans Eclipse n'aura pas de répertoire racine de test. Cela a été ajouté à partir des Propriétés du projet > Chemin de construction Java > Ajouter un dossier… > Créer un nouveau dossier… > spécifiez le nom du dossier > Terminer. Le nouveau répertoire sera sélectionné comme dossier source. Cliquez sur OK dans les deux boîtes de dialogue restantes.
Les tests JUnit 4 peuvent être créés avec File > New > JUnit Test Case. Sélectionnez "Nouveau test JUnit 4" et le dossier source nouvellement créé pour les tests. Spécifiez une "classe sous test" et un "package", en vous assurant que le package correspond à la classe sous test. Ensuite, spécifiez un nom pour la classe de test. Après avoir terminé l'assistant, si vous y êtes invité, choisissez "Ajouter la bibliothèque JUnit 4" au chemin de construction. Le projet ou la classe de test individuelle peut ensuite être exécuté en tant que test JUnit. Voir aussi Eclipse Writing et Exécution de tests JUnit.
NetBeans
NetBeans ne prend en charge que les tests JUnit 4. Les classes de test peuvent être créées dans un projet Java NetBeans avec File > New File… > Unit Tests > JUnit Test ou Test for Existing Class. Par défaut, le répertoire racine de test est nommé test
dans le répertoire du projet.
Classe de production simple et son cas de test JUnit
Examinons un exemple simple de code de production et son code de test unitaire correspondant pour une classe Person
très simple. Vous pouvez télécharger l'exemple de code de mon projet github et l'ouvrir via IntelliJ.
src/main/java/com/example/Person.java
package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ", " + givenName; } }
La classe immuable Person
a un constructeur et une méthode getDisplayName()
. Nous voulons tester que getDisplayName()
renvoie le nom formaté comme prévu. Voici le code de test pour un test unitaire unique (JUnit 5):
src/test/java/com/example/PersonTest.java
package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person("Josh", "Hayden"); String displayName = person.getDisplayName(); assertEquals("Hayden, Josh", displayName); } }
PersonTest
utilise @Test
et l'assertion de JUnit 5. Pour JUnit 4, la classe et la méthode PersonTest
doivent être publiques et différentes importations doivent être utilisées. Voici l'exemple JUnit 4 Gist.
Lors de l'exécution de la classe PersonTest
dans IntelliJ, le test réussit et les indicateurs de l'interface utilisateur sont verts.
Conventions JUnit courantes
Appellation
Bien que cela ne soit pas obligatoire, nous utilisons des conventions communes pour nommer la classe de test ; plus précisément, nous commençons par le nom de la classe testée ( Person
) et y ajoutons "Test" ( PersonTest
). Nommer la méthode de test est similaire, en commençant par la méthode testée ( getDisplayName()
) et en y ajoutant « test » ( testGetDisplayName()
). Bien qu'il existe de nombreuses autres conventions parfaitement acceptables pour nommer les méthodes de test, il est important d'être cohérent au sein de l'équipe et du projet.
Nom en production | Nom dans les tests |
---|---|
La personne | Test de personne |
getDisplayName() | testDisplayName() |
Paquets
Nous utilisons également la convention de création de la classe PersonTest
du code de test dans le même package ( com.example
) que la classe Person
du code de production. Si nous utilisions un package différent pour les tests, nous serions obligés d'utiliser le modificateur d'accès public dans les classes de code de production, les constructeurs et les méthodes référencées par les tests unitaires, même là où ce n'est pas approprié, il est donc préférable de les conserver dans le même package . Cependant, nous utilisons des répertoires source séparés ( src/main/java
et src/test/java
) car nous ne souhaitons généralement pas inclure de code de test dans les versions de production publiées.
Structure et annotation
L'annotation @Test
(JUnit 4/5) indique à JUnit d'exécuter la méthode testGetDisplayName()
en tant que méthode de test et de signaler si elle réussit ou échoue. Tant que toutes les assertions (le cas échéant) réussissent et qu'aucune exception n'est levée, le test est considéré comme réussi.
Notre code de test suit le modèle de structure de Arrange-Act-Assert (AAA). D'autres modèles courants incluent Given-When-Then et Setup-Exercise-Verify-Teardown (Teardown n'est généralement pas explicitement nécessaire pour les tests unitaires), mais nous utilisons AAA dans cet article.
Voyons comment notre exemple de test suit AAA. La première ligne, le "arrange" crée un objet Person
qui sera testé :
Person person = new Person("Josh", "Hayden");
La deuxième ligne, "act", exerce la méthode Person.getDisplayName()
du code de production :
String displayName = person.getDisplayName();
La troisième ligne, le "assert", vérifie que le résultat est comme prévu.
assertEquals("Hayden, Josh", displayName);
En interne, l'appel assertEquals()
utilise la méthode equals de l'objet String "Hayden, Josh" pour vérifier que la valeur réelle renvoyée par le code de production ( displayName
) correspond. S'il ne correspondait pas, le test aurait été marqué comme échoué.
Notez que les tests ont souvent plus d'une ligne pour chacune de ces phases AAA.
Tests unitaires et code de production
Maintenant que nous avons couvert certaines conventions de test, tournons notre attention vers la possibilité de tester le code de production.
Nous revenons à notre classe Person
, où j'ai implémenté une méthode pour renvoyer l'âge d'une personne en fonction de sa date de naissance. Les exemples de code nécessitent Java 8 pour tirer parti des nouvelles API fonctionnelles et de date. Voici à quoi ressemble la nouvelle classe Person.java
:
Personne.java
// ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }
L'exécution de cette classe (au moment de la rédaction) annonce que Joey a 4 ans. Ajoutons une méthode de test :
PersonTest.java
// ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); long age = person.getAge(); assertEquals(4, age); } }
Il passe aujourd'hui, mais qu'en sera-t-il lorsque nous l'exécuterons dans un an ? Ce test est non déterministe et fragile car le résultat attendu dépend de la date actuelle du système exécutant le test.
Écraser et injecter un fournisseur de valeur
Lors de l'exécution en production, nous voulons utiliser la date actuelle, LocalDate.now()
, pour calculer l'âge de la personne, mais pour effectuer un test déterministe même dans un an, les tests doivent fournir leurs propres valeurs currentDate
.
C'est ce qu'on appelle l'injection de dépendance. Nous ne voulons pas que notre objet Person
détermine lui-même la date actuelle, mais nous voulons plutôt transmettre cette logique en tant que dépendance. Les tests unitaires utiliseront une valeur stub connue et le code de production permettra à la valeur réelle d'être fournie par le système au moment de l'exécution.
Ajoutons un fournisseur LocalDate
à Person.java
:
Personne.java
// ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier<LocalDate> currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }
Pour faciliter le test de la méthode getAge()
, nous l'avons modifiée pour utiliser currentDateSupplier
, un fournisseur LocalDate
, pour récupérer la date actuelle. Si vous ne savez pas ce qu'est un fournisseur, je vous recommande de lire sur les interfaces fonctionnelles intégrées Lambda.
Nous avons également ajouté une injection de dépendance : le nouveau constructeur de test permet aux tests de fournir leurs propres valeurs de date actuelle. Le constructeur d'origine appelle ce nouveau constructeur, en passant une référence de méthode statique de LocalDate::now
, qui fournit un objet LocalDate
, de sorte que notre méthode principale fonctionne toujours comme avant. Qu'en est-il de notre méthode de test ? Mettons à jour PersonTest.java
:
PersonTest.java
// ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse("2013-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-17"); Person person = new Person("Joey", "Doe", dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }
Le test injecte maintenant sa propre valeur currentDate
, donc notre test réussira quand il sera exécuté l'année prochaine, ou au cours de n'importe quelle année. Ceci est communément appelé stubbing ou fournir une valeur connue à renvoyer, mais nous avons d'abord dû changer Person
pour permettre à cette dépendance d'être injectée.
Notez la syntaxe lambda ( ()->currentDate
) lors de la construction de l'objet Person
. Ceci est traité comme un fournisseur d'un LocalDate
, comme requis par le nouveau constructeur.
Se moquer et stubber un service Web
Nous sommes prêts pour que notre objet Person
, dont toute l'existence a été dans la mémoire JVM, communique avec le monde extérieur. Nous voulons ajouter deux méthodes : la méthode publishAge()
, qui affichera l'âge actuel de la personne, et la méthode getThoseInCommon()
, qui renverra les noms de personnes célèbres qui partagent le même anniversaire ou qui ont le même âge que notre Person
. Supposons qu'il existe un service RESTful avec lequel nous pouvons interagir appelé "People Birthdays". Nous avons un client Java pour cela qui se compose de la classe unique, BirthdaysClient
.
com.example.birthdays.BirthdaysClient
package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println("publishing " + name + "'s age: " + age); // HTTP POST with name and age and possibly throw an exception } public Collection<String> findFamousNamesOfAge(long age) throws IOException { System.out.println("finding famous names of age " + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection<String> findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println("finding famous names born on day " + dayOfMonth + " of month " + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }
Améliorons notre classe Person
. Nous commençons par ajouter une nouvelle méthode de test pour le comportement souhaité de publishAge()
. Pourquoi commencer par le test plutôt que par la fonctionnalité ? Nous suivons les principes du développement piloté par les tests (également connu sous le nom de TDD), dans lequel nous écrivons d'abord le test, puis le code pour le faire passer.
PersonTest.java
// … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate); person.publishAge(); } }
À ce stade, le code de test ne parvient pas à se compiler car nous n'avons pas créé la méthode publishAge()
qu'il appelle. Une fois que nous avons créé une méthode Person.publishAge()
vide, tout passe. Nous sommes maintenant prêts pour le test afin de vérifier que l'âge de la personne est bien publié sur le BirthdaysClient
.

Ajout d'un objet simulé
Comme il s'agit d'un test unitaire, il doit s'exécuter rapidement et en mémoire, de sorte que le test construira notre objet Person
avec un mock BirthdaysClient
afin qu'il ne fasse pas réellement de requête Web. Le test utilisera ensuite cet objet fictif pour vérifier qu'il a été appelé comme prévu. Pour ce faire, nous allons ajouter une dépendance au framework Mockito (licence MIT) pour créer des objets mock, puis créer un objet BirthdaysClient
mock :
PersonTest.java
// ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); // ... } }
Nous avons en outre augmenté la signature du constructeur Person
pour prendre un objet BirthdaysClient
, et modifié le test pour injecter l'objet BirthdaysClient
simulé.
Ajouter une attente fictive
Ensuite, nous ajoutons à la fin de notre testPublishAge
une attente que le BirthdaysClient
soit appelé. Person.publishAge()
doit l'appeler, comme indiqué dans notre nouveau PersonTest.java
:
PersonTest.java
// ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); } }
Notre BirthdaysClient
amélioré par Mockito garde une trace de tous les appels qui ont été faits à ses méthodes, c'est ainsi que nous vérifions qu'aucun appel n'a été fait à BirthdaysClient
avec la méthode verifyZeroInteractions()
avant d'appeler publishAge()
. Bien que ce ne soit sans doute pas nécessaire, en faisant cela, nous nous assurons que le constructeur ne fait aucun appel malveillant. Sur la ligne verify()
, nous spécifions à quoi nous attendons l'appel à BirthdaysClient
.
Notez que, comme publishRegularPersonAge a l'exception IOException dans sa signature, nous l'ajoutons également à notre signature de méthode de test.
À ce stade, le test échoue :
Wanted but not invoked: birthdaysClient.publishRegularPersonAge( "Joe Sixteen", 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)
Cela est normal, étant donné que nous n'avons pas encore implémenté les modifications requises dans Person.java
, car nous suivons un développement piloté par les tests. Nous allons maintenant faire passer ce test en apportant les modifications nécessaires :
Personne.java
// ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + " " + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }
Test des exceptions
Nous avons fait en sorte que le constructeur de code de production instancie un nouveau BirthdaysClient
, et publishAge()
appelle maintenant le birthdaysClient
. Tous les tests réussissent ; tout est vert. Génial! Mais notez que publishAge()
avale l'IOException. Au lieu de le laisser bouillonner, nous voulons l'envelopper avec notre propre PersonException dans un nouveau fichier appelé PersonException.java
:
PersonException.java
package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }
Nous implémentons ce scénario en tant que nouvelle méthode de test dans PersonTest.java
:
PersonTest.java
// ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); try { person.publishAge(); fail("expected exception not thrown"); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals("Failed to publish Joe Sixteen age 16", e.getMessage()); } } }
L'appel Mockito doThrow()
stubs birthdaysClient
pour lever une exception lorsque la méthode publishRegularPersonAge()
est appelée. Si l' PersonException
n'est pas levée, nous échouons au test. Sinon, nous affirmons que l'exception a été correctement chaînée avec IOException et vérifions que le message d'exception est comme prévu. À l'heure actuelle, comme nous n'avons implémenté aucune gestion dans notre code de production, notre test échoue car l'exception attendue n'a pas été levée. Voici ce que nous devons changer dans Person.java
pour que le test réussisse :
Personne.java
// ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException("Failed to publish " + nameToPublish + " age " + age, e); } } }
Stubs : Quand et Assertions
Nous implémentons maintenant la méthode Person.getThoseInCommon()
, faisant ressembler notre classe Person.Java
à ceci.
Notre testGetThoseInCommon()
, contrairement à testPublishAge()
, ne vérifie pas que des appels particuliers ont été effectués aux méthodes birthdaysClient
. Au lieu de cela, il utilise when
les appels à stub renvoient des valeurs pour les appels à findFamousNamesOfAge()
et findFamousNamesBornOn()
que getThoseInCommon()
devra effectuer. Nous affirmons ensuite que les trois noms tronqués que nous avons fournis sont renvoyés.
Envelopper plusieurs assertions avec la assertAll()
JUnit 5 permet à toutes les assertions d'être vérifiées dans leur ensemble, plutôt que de s'arrêter après la première assertion ayant échoué. Nous incluons également un message avec assertTrue()
pour identifier les noms particuliers qui ne sont pas inclus. Voici à quoi ressemble notre méthode de test "chemin heureux" (un scénario idéal) (notez qu'il ne s'agit pas d'un ensemble de tests robustes par nature d'être un "chemin heureux", mais nous expliquerons pourquoi plus tard.
PersonTest.java
// ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList("JoeFamous Sixteen", "Another Person")); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList("Jan TwoKnown")); Set<String> thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, "Another Person"), setContains(thoseInCommon, "Jan TwoKnown"), setContains(thoseInCommon, "JoeFamous Sixteen"), ()-> assertEquals(3, thoseInCommon.size()) ); } private <T> Executable setContains(Set<T> set, T expected) { return () -> assertTrue(set.contains(expected), "Should contain " + expected); } // ... }
Gardez le code de test propre
Bien que souvent négligé, il est tout aussi important de garder le code de test exempt de duplication purulente. Un code propre et des principes tels que "ne vous répétez pas" sont très importants pour maintenir une base de code de haute qualité, tant pour la production que pour le code de test. Notez que le plus récent PersonTest.java a des doublons maintenant que nous avons plusieurs méthodes de test.
Pour résoudre ce problème, nous pouvons faire plusieurs choses :
Extrayez l'objet IOException dans un champ final privé.
Extrayez la création de l'objet
Person
dans sa propre méthode (createJoeSixteenJan2()
, dans ce cas) puisque la plupart des objets Person sont créés avec les mêmes paramètres.Créez un
assertCauseAndMessage()
pour les différents tests qui vérifientPersonExceptions
lancées.
Les résultats du code propre peuvent être vus dans cette interprétation du fichier PersonTest.java.
Testez plus que le chemin heureux
Que faire lorsqu'un objet Person
a une date de naissance postérieure à la date actuelle ? Les défauts dans les applications sont souvent dus à une entrée inattendue ou à un manque de prévoyance dans les cas de coin, de bord ou de limite. Il est important d'essayer d'anticiper au mieux ces situations, et les tests unitaires sont souvent un endroit approprié pour le faire. Lors de la construction de nos Person
et PersonTest
, nous avons inclus quelques tests pour les exceptions attendues, mais ce n'était en aucun cas complet. Par exemple, nous utilisons LocalDate
qui ne représente ni ne stocke les données de fuseau horaire. Nos appels à LocalDate.now()
, cependant, renvoient une LocalDate
basée sur le fuseau horaire par défaut du système, qui peut être un jour plus tôt ou plus tard que celui de l'utilisateur d'un système. Ces facteurs doivent être pris en compte avec des tests et un comportement appropriés mis en œuvre.
Les limites doivent également être testées. Considérez un objet Person
avec une méthode getDaysUntilBirthday()
. Les tests doivent inclure si oui ou non l'anniversaire de la personne est déjà passé dans l'année en cours, si l'anniversaire de la personne est aujourd'hui et comment une année bissextile affecte le nombre de jours. Ces scénarios peuvent être couverts en vérifiant un jour avant l'anniversaire de la personne, le jour de et un jour après l'anniversaire de la personne où l'année suivante est une année bissextile. Voici le code de test pertinent :
PersonTest.java
// ... class PersonTest { private final Supplier<LocalDate> currentDateSupplier = ()-> LocalDate.parse("2015-05-02"); private final LocalDate ageJustOver5 = LocalDate.parse("2010-05-01"); private final LocalDate ageExactly5 = LocalDate.parse("2010-05-02"); private final LocalDate ageAlmost5 = LocalDate.parse("2010-05-03"); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function<Person, Long> personLongFunction) { Person person = new Person("Given", "Sur", dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }
Essais d'intégration
Nous nous sommes principalement concentrés sur les tests unitaires, mais JUnit peut également être utilisé pour les tests d'intégration, d'acceptation, fonctionnels et système. De tels tests nécessitent souvent plus de code de configuration, par exemple, démarrer des serveurs, charger des bases de données avec des données connues, etc. Bien que nous puissions souvent exécuter des milliers de tests unitaires en quelques secondes, les grandes suites de tests d'intégration peuvent prendre des minutes, voire des heures. Les tests d'intégration ne doivent généralement pas être utilisés pour essayer de couvrir chaque permutation ou chemin à travers le code ; les tests unitaires sont plus appropriés pour cela.
La création de tests pour les applications Web qui pilotent les navigateurs Web en remplissant des formulaires, en cliquant sur des boutons, en attendant le chargement du contenu, etc., est généralement effectuée à l'aide de Selenium WebDriver (licence Apache 2.0) couplé au 'Page Object Pattern' (voir le wiki SeleniumHQ github et l'article de Martin Fowler sur les objets de page).
JUnit est efficace pour tester les API RESTful avec l'utilisation d'un client HTTP comme Apache HTTP Client ou Spring Rest Template (HowToDoInJava.com fournit un bon exemple).
Dans notre cas avec l'objet Person
, un test d'intégration pourrait impliquer l'utilisation du vrai BirthdaysClient
plutôt qu'un faux, avec une configuration spécifiant l'URL de base du service People Birthdays. Un test d'intégration utiliserait alors une instance de test d'un tel service, vérifierait que les anniversaires y ont été publiés et créerait des personnes célèbres dans le service qui seraient renvoyées.
Autres fonctionnalités de JUnit
JUnit possède de nombreuses fonctionnalités supplémentaires que nous n'avons pas encore explorées dans les exemples. Nous en décrirons certains et fournirons des références pour d'autres.
Appareils d'essai
Il convient de noter que JUnit crée une nouvelle instance de la classe de test pour exécuter chaque méthode @Test
. JUnit fournit également des crochets d'annotation pour exécuter des méthodes particulières avant ou après toutes ou chacune des méthodes @Test
. Ces crochets sont souvent utilisés pour configurer ou nettoyer une base de données ou des objets fictifs, et diffèrent entre JUnit 4 et 5.
JUnit 4 | JUnit 5 | Pour une méthode statique ? |
---|---|---|
@BeforeClass | @BeforeAll | Oui |
@AfterClass | @AfterAll | Oui |
@Before | @BeforeEach | Non |
@After | @AfterEach | Non |
Dans notre exemple PersonTest
, nous avons choisi de configurer l'objet factice BirthdaysClient
dans les méthodes @Test
elles-mêmes, mais il est parfois nécessaire de créer des structures factices plus complexes impliquant plusieurs objets. @BeforeEach
(dans JUnit 5) et @Before
(dans JUnit 4) sont souvent appropriés pour cela.
Les annotations @After*
sont plus courantes avec les tests d'intégration qu'avec les tests unitaires, car le ramasse-miettes JVM gère la plupart des objets créés pour les tests unitaires. Les annotations @BeforeClass
et @BeforeAll
sont le plus souvent utilisées pour les tests d'intégration qui doivent effectuer des actions de configuration et de démontage coûteuses une seule fois, plutôt que pour chaque méthode de test.
Pour JUnit 4, veuillez vous référer au guide des appareils de test (les concepts généraux s'appliquent toujours à JUnit 5).
Suites de tests
Parfois, vous souhaitez exécuter plusieurs tests liés, mais pas tous les tests. Dans ce cas, les regroupements de tests peuvent être composés en suites de tests. Pour savoir comment procéder dans JUnit 5, consultez l'article JUnit 5 de HowToProgram.xyz et la documentation de l'équipe JUnit pour JUnit 4.
@Nested et @DisplayName de JUnit 5
JUnit 5 ajoute la possibilité d'utiliser des classes internes imbriquées non statiques pour mieux montrer la relation entre les tests. Cela devrait être très familier à ceux qui ont travaillé avec des descriptions imbriquées dans des frameworks de test comme Jasmine pour JavaScript. Les classes internes sont annotées avec @Nested
pour l'utiliser.
L'annotation @DisplayName
est également nouvelle dans JUnit 5, vous permettant de décrire le test pour les rapports sous forme de chaîne, à afficher en plus de l'identifiant de la méthode de test.
Bien que @Nested
et @DisplayName
puissent être utilisés indépendamment l'un de l'autre, ensemble, ils peuvent fournir des résultats de test plus clairs qui décrivent le comportement du système.
Matchers Hamcrest
Le framework Hamcrest, bien qu'il ne fasse pas lui-même partie de la base de code JUnit, offre une alternative à l'utilisation des méthodes d'assertion traditionnelles dans les tests, permettant un code de test plus expressif et lisible. Voir la vérification suivante utilisant à la fois un assertEquals traditionnel et un assertThat Hamcrest :
//Traditional assert assertEquals("Hayden, Josh", displayName); //Hamcrest assert assertThat(displayName, equalTo("Hayden, Josh"));
Hamcrest peut être utilisé avec JUnit 4 et 5. Le tutoriel de Vogella.com sur Hamcrest est assez complet.
Ressources supplémentaires
L'article Tests unitaires, comment écrire du code testable et pourquoi c'est important couvre des exemples plus spécifiques d'écriture de code propre et testable.
Build with Confidence : A Guide to JUnit Tests examine différentes approches des tests unitaires et d'intégration, et explique pourquoi il est préférable d'en choisir une et de s'y tenir
Le wiki de JUnit 4 et le guide de l'utilisateur de JUnit 5 sont toujours un excellent point de référence.
La documentation Mockito fournit des informations sur des fonctionnalités supplémentaires et des exemples.
JUnit est la voie vers l'automatisation
Nous avons exploré de nombreux aspects des tests dans le monde Java avec JUnit. Nous avons examiné les tests unitaires et d'intégration à l'aide du framework JUnit pour les bases de code Java, l'intégration de JUnit dans les environnements de développement et de construction, l'utilisation des simulations et des stubs avec les fournisseurs et Mockito, les conventions courantes et les meilleures pratiques de code, ce qu'il faut tester et certains des d'autres fonctionnalités géniales de JUnit.
C'est maintenant au tour du lecteur de grandir dans l'application, la maintenance et la récolte habiles des avantages des tests automatisés à l'aide du framework JUnit.