Le projet Junit a été en suspend pendant pas mal de temps (version 4 a été créée en 2006 et la dernière grosse mise à jour date de 2011) mais il reste une des librairies Java les plus utilisées quelque soit les projets. Quelques personnes ont repris le projet en main pour écrire une librairie offrant toutes les possibilités de Java 8 et beaucoup plus modulaire.
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform est le socle pour lancer des frameworks de tests sur la JVM. Une API a été définie et chacun est libre de l’implémenter. Ce module contient aussi tous les plugins pour pouvoir lancer des tests et notamment les plugins Maven et Gradle.
JUnit Jupiter est une implémentation de l’API définie dans JUnit Platform
JUnit Vintage est le projet qui permet d’assurer la rétrocompatibilité avec Junit 4 et Junit 3
Nous allons voir comment utiliser cette nouvelle version dans un projet Spring Boot 2. Les sources du code montré ici, sont disponibles sous Github.
Ce projet exemple est minimaliste et comprend un bean de propriétés qui va injecter la propriété devmin.name
dans le code et notre test consiste à vérifier que ce bean est bien peuplé
@ConfigurationProperties("devmind")
public class Junit5ApplicationProperties {
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
Ce bean est initialisé dans le bean de bootstrap de l’application
@SpringBootApplication
@EnableConfigurationProperties(Junit5ApplicationProperties.class)
public class Junit5Application {
public static void main(String[] args) {
SpringApplication.run(Junit5Application.class, args);
}
}
Et le test Junit 4 peut être écrit de cette manière. J’ai indiqué les packages pour que vous puissiez voir les différences plus loin
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class Junit5ApplicationTests {
@Autowired
private Junit5ApplicationProperties properties;
@Test
public void contextLoads() {
Assertions.assertThat(properties.getName()).isEqualTo("Dev-Mind");
}
}
Nous allons maintenant écrire ce même test avec Junit 5.
Votre descripteur de build Gradle (build.gradle
) doit pour le moment ressembler à
buildscript {
ext {
springBootVersion = '2.0.0.M7'
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/milestone" }
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'com.devmind'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
maven { url "https://repo.spring.io/milestone" }
}
dependencies {
compile('org.springframework.boot:spring-boot-starter')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
Nous allons indiquer à Gradle qu’il doit utiliser le plugin JUnit Platform
.
classpath("org.junit.platform:junit-platform-gradle-plugin:${junitPlatformVersion}")
apply plugin: "org.junit.platform.gradle.plugin"
Pour être sûr de ne plus utiliser l’ancienne version de Junit, nous allons exclure la dépendance junit tirée par spring-boot-starter-test
testCompile("org.springframework.boot:spring-boot-starter-test") {
exclude module: "junit"
}
ou même mieux vous pouvez le faire d’une manière globale
configurations {
all {
exclude module: "junit"
}
}
Nous devons maintenant ajouter les dépendances Junit5 pour que notre projet puisse lancer les tests
testCompile("org.junit.jupiter:junit-jupiter-api")
testRuntime("org.junit.jupiter:junit-jupiter-engine")
Votre code avec les dépendances Junit4 ne doit plus compiler! Vous pouvez maintenant lire la documentation Junit sur comment migrer de Junit4 à Junit5.
Quand vous avez un gros projet vous voulez peut être faire cohabiter les 2 versions et migrer au fur et à mesure vos tests. Dans ce cas, gardez la dépendance junit pour que votre code compile et ajouter la dépendance suivante dans votre script Gradle
testRuntime("org.junit.vintage:junit-vintage-engine:4.12.2")
Pour la partie pure Junit, vous pouvez suivre la documentation officielle. Pour résumer voici les principales évolutions
Les annotations, les assertions et les hypothèses (Assumptions) ont été déplacées dans le package org.junit.jupiter.api
. Personnellement je n’utilise pas les assertions Junit et je préfère les assertions offertes par le projet AssertJ. Pour les Assumptions je ne suis pas fan car je préfère qu’un test en échec soit bloquant.
Les annotations @Before
et @After
ont été remplacées par @BeforeEach
et @AfterEach
Les annotations @BeforeClass
et @AfterClass
ont été remplacées par @BeforeAll
et @AfterAll
L’annotation @Ignore
a été remplacée par @Disabled
. Petite remarque, un projet ne devrait pas avoir de test ignorés. S’ils ne sont plus valides ils doivent être supprimés.
Les catégories @Category
ont été remmplacées par les @Tag
L’annotation @RunWith
est remplacée par @ExtendWith
Ces renommages permettent d’avoir des noms d’annotation plus parlant. Par contre vous mixez peut être des tests TestNg et des tests Junit dans vos projets ? Si c’est le cas je vous conseille aussi de migrer vos tests TestNg vers Junit. TestNg était très intéressant il y a quelques années quand il permettait de combler les manques de Junit. Aujourd’hui le projet ne bouge plus beaucoup.
Pour les amoureux des règles Junit, elles ne sont pas encore disponibles. Pour rappel, elles permettaient de combler les manques de Junit où on ne pouvait pas faire de composition de @RunWith
. En gros une fois que vous aviez déclaré @RunWith(SpringRunner.class)
sur votre classe vous ne pouviez pas ajouter un @RunWith(MockitoJUnitRunner.class)
. Les règles Junit vous offrait un moyen simple de factoriser du comportement entre les tests.
Mais Junit5 s’appuie sur Java 8 et n’est d’ailleurs pas compatible avec les versions antérieures. Depuis Java 8 des annotations peuvent être "Repeatable". C’est le cas de ExtendWith. Vous pouvez maintenant écrire par exemple
@ExtendWith(MockitoExtension.class)
@ExtendWith(SpringExtension.class)
Spring Boot propose plusieurs annotations pour les tests et elles sont toujours utilisables. Vous pouvez suivre la doc officielle mais nous allons voir comment migrer notre exemple présenté au début de cet article.
Mon exemple devient
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class Junit5ApplicationTests {
@Autowired
private Junit5ApplicationProperties properties;
@Test
public void contextLoads() {
Assertions.assertThat(properties.getName()).isEqualTo("Dev-Mind");
}
}
Si vous rencontrez des problèmes avec IntelliJ pour lancer les tests je vous laisse lire ce post sur le forum de Gradle et celui-ci sur le site de JetBrains.
Au niveau de Spring ne passez pas à côté des annotations composées qui peuvent aider à la lecture de vos tests. Par exemple si vous utilisez toujours une multitude d’annotation sur vos tests comme dans cet exemple issu de la doc de Spring
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
Vous pouvez créer une annotation
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
et reprendre vos tests pour ne plus avoir que
@TransactionalDevTestConfig
class OrderRepositoryTests { }
@TransactionalDevTestConfig
class UserRepositoryTests { }
Après avoir vu comment migrer des tests existants, nous pouvons maintenant nous attarder sur quelques nouvelles fonctionnalités
A force d’écrire des tests en JavaScript, j’étais toujours frustré du code écrit en Junit4. Pour avoir des rapports d’exécution lisible on essaye d’utiliser des noms à rallonge dans les méthodes des tests.
Par exemple si nous voulions tester cette interface
public interface CallForPaperService {
void submit(Proposal proposal);
void accept(Proposal proposal);
void refuse(Proposal proposal);
}
On pourrait imaginer les tests suivants
class CallForPaperServiceTests {
@Test
public void submitShouldFailWhenRequiredDataAreAbsent(){ }
@Test
public void submitShouldFailWhenConfirmationMailIsNtSend(){ }
@Test
public void submitShouldSendEmail(){ }
@Test
public void acceptShouldSendEmailToSessionSpeakers(){ }
@Test
public void acceptShouldFailWhenSpeakerEmailCantBeSend(){ }
// ... on peut imaginer des dizaines de tests supplémentaires avec des noms beaucoup plus long
}
Ce qui donnerait le rapport suivant
En Javascript vous pouvez écrire
it('submit should fail when required data are absent', () => { });
Vous pouvez migrer votre code en Kotlin qui permet de définir des méthodes avec des phrases :-)
@Test
fun `submit should fail when required data are absent`() { }
Maintenant avec Junit5 vous pourrez ajouter l’annotation @DisplayName
et dissocier les messages attendus dans les rapports des noms de vos méthode. Par exemple
@DisplayName("Test service CallForPaperService")
class CallForPaperServiceTests {
@Test
@DisplayName("submit should fail when required data are absent")
public void submitFailOnBadArgument(){ }
@Test
@DisplayName("submit should fail when confirmation email is not send")
public void submitFailOnEmailError(){ }
@Test
@DisplayName("submit should send email")
public void submitSendEmail(){ }
@Test
@DisplayName("accept should send email to session speakers")
public void acceptSendEmailToSessionSpeakers(){ }
@Test
@DisplayName("accept should fail when speaker email can't be send")
public void acceptFailOnEmailError(){ }
}
Ce qui donnerait le rapport suivant
Nous avons résolu un premier problème. Quand vous multipliez les tests vous ne savez pas forcément à quelle méthode de votre classe testée se réfère. En Javascript avec Jasmine, vous pouvez faire des sous suites de tests au sein d’une suite de tests. Maintenant avec @Nested vous allez pouvoir faire la même chose
@DisplayName("Test service CallForPaperService")
class CallForPaperServiceTests {
@Nested
@DisplayName("submit")
class Submit{
@Test
@DisplayName("should fail when required data are absent")
public void submitFailOnBadArgument(){ }
@Test
@DisplayName("should fail when confirmation email is not send")
public void submitFailOnEmailError(){ }
@Test
@DisplayName("should send email")
public void submitSendEmail(){ }
}
@Nested
@DisplayName("accept")
class Accept{
@Test
@DisplayName("should send email to session speakers")
public void acceptSendEmailToSessionSpeakers(){ }
@Test
@DisplayName("should fail when speaker email can't be send")
public void acceptFailOnEmailError(){ }
}
}
Ce code donnera en sortie
Au final nous avons un code plus verbeux mais l’organisation permet d’avoir quelque chose de beaucoup plus lisible que ce soit au niveau du code même, des tests, mais ausi des rapports.
Quand nous voulions exécuter plusieurs fois un même test pour vérifier la performance ou autre, nous devions batailler avec les anciennes versions de Junit, ou utiliser l’annotation Repeat de spring-test
ou alors écrire des tests avec d’autres frameworks comme TestNg par exemple.
Maintenant rien de plus simple vous écrivez
@Test
@DisplayName("should send email to session speakers")
@RepeatedTest(10)
public void acceptSendEmailToSessionSpeakers(){
assertThat(true).isTrue();
}
Et en sortie vous aurez votre test exécuté 1 fois et répeter 10 fois
Vous pouviez ajouter une catégorie à vos tests avec la version précédente de Junit. Par exemple
@Category({IntegrationTest.class, Exernal.class})
Avec Junit 5 vous pouvez maintenant utiliser l’annotation @Tag
@Tag("integration")
@Tag("exernal")
Ces tags peuvent ensuite jouer sur le runtime
Quand vous configurez Gradle et le plugin junitPlatform vous pouvez spécifier plusieurs options comme les tags exclus ou inclus
junitPlatform {
filters {
tags {
include 'fast', 'smoke'
exclude 'slow', 'ci'
}
packages {
include 'com.sample.included1', 'com.sample.included2'
}
includeClassNamePattern '.*Spec'
includeClassNamePatterns '.*Test', '.*Tests'
}
}
Comme nous avons pu le voir vous pouvez dès aujourd’hui adopter Junit 5 dans vos projets Spring Boot ou autre projet Java. Cette refonte de Junit apporte à mon sens plein de petits plus dans l’écriture des tests. D’autres évolutions qui sont encore au stade expérimentations peuvent être utilisées par parcimonie. Mais rien ne dit si elles seront conservées ou non dans les futures versions (voir la liste)