Dev-Mind

Utiliser Junit 5 dans une application Spring Boot

08/01/2018
Java  Junit  Spring Boot 

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

Junit dans SpringBoot

Nous allons voir comment utiliser cette nouvelle version dans un projet Spring Boot 2. Les sources du code montré ici, sont disponibles sous Github.

Description de l’exemple

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.

Paramétrer Gradle pour pouvoir utiliser Junit 5 dans un projet Spring Boot

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")

Comment migrer ces tests Junit4 à Junit5 ?

Pour la partie pure Junit, vous pouvez suivre la documentation officielle. Pour résumer voici les principales évolutions

Renommages et changements de package

  • 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.

Les Rules Junit

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)

Par contre MockitoExtension n’existe pas encore. Vous pouvez voir cette issue Github et MockitoExtension devrait arriver avec Mockito 3.0

Qu’en est il de la partie Spring Boot ?

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 { }

Les fonctionnalités sympas de Junit 5

Après avoir vu comment migrer des tests existants, nous pouvons maintenant nous attarder sur quelques nouvelles fonctionnalités

Améliorer la lisibité de ses tests avec @Nested et @DisplayName

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

Suite de tests

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

Suite de tests avec DisplayName

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

Suite de tests avec Nested

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.

Répeter les tests

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

Suite de tests avec RepeatedTest

Les tags

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")

N’oubliez pas les annotation composées. Par exemple ici on pourrait créer une annotation @IntegrationTestWithExternalSystem pour jumeler ces tags

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'
    }
}

Conclusion

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)


Commentaires