JUnit 5

Introduction

Junit 4 a sorti sa première version en février 2006 et sa dernière (4.12) en Décembre 2014. Il était donc grand temps de sortir une nouvelle version pour s'adapter aux standards de JAVA 8 (Et JAVA 9 qui vient de sortir) et aux besoins des développeurs en matière de tests. Cette version tant attendue est donc enfin sortie le 10 septembre 2017 et je vais donc vous présenter les grands axes de cette version et les choses qui ont retenu mon attention.

Présentation

JUnit 5 n'est plus composé d'un package unique; il est désormais découpé en 3 modules principaux qui nous permettent de n'installer que ceux dont nous avons besoin.

Ces trois packages sont les suivants :

  • JUnit Platform : contient tout l’aspect « moteur » de JUnit. Utilisé pour exécuter les tests.
  • JUnit Jupiter : combine une API et des mécanismes d’extension. Utilisé dans les tests unitaires.
  • JUnit Vintage : fournit un moyen d’exécuter les tests unitaires existants initialement écrits pour JUnit 3 et 4

Ce découpage favorise ainsi l'intégration de JUnit dans les outils de développement au sens large du terme : IDE, build (gradle, maven) et intégration continue.

Les noms de packages ne sont bien sûr pas les mêmes que ceux de JUnit 4 afin de pouvoir combiner les tests Junit 4 et Junit 5. Cela nous permettra de migrer beaucoup plus sereinement nos tests et facilitera la réécritue de ceux-ci.

Junit 5 requiert bien évidement JAVA 8

 

Compatibilité IDE

IntelliJIDEA Version BundledJUnit 5 Version
2016.2​ M2​
2016.3.1​ M3​
2017.1.2​ M4​
2017.2.1​ M5​
2017.2.3​ RC2​
2017.2.4​ 5.0.0

Pour Eclipse, la v5 de JUnit est compatible en beta dans la version 4.7 (Oxygen) à l'heure où j'écris cet article.

Concernant les autres IDE, rien n'a l'air de prévu pour le moment dans ce que j'ai pu lire.

 

Installation

Voici un exemple de configuration Gradle :

group 'bzh.netapsys'
version '1.0.0'

ext.junitPlatformVersion = '1.0.0'
ext.junitJupiterVersion = '5.0.0'

buildscript {
    repositories {
        mavenCentral()
        // The following is only necessary if you want to use SNAPSHOT releases.
        // maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
    }
    dependencies {
        classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0-RC2'
        classpath 'org.junit.platform:junit-platform-commons:1.0.0-RC2'
    }
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.junit.platform.gradle.plugin'

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8

junitPlatform {
    platformVersion '1.0.0'
//    logManager 'org.apache.logging.log4j.jul.LogManager'
    logManager 'java.util.logging.LogManager'
    reportsDir file('build/test-results/junit-platform') // this is the default
    // enableStandardTestTask true
    // selectors (optional)
    // filters (optional)
}

dependencies {
    // Needed as compile since we're building a JUnit Jupiter extension here
    compile "org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion"

    compile 'org.junit.platform:junit-platform-commons:1.0.0'
    compile 'org.mockito:mockito-core:2.+'

    /***** TESTS *****/
    // Needed for @RunWith(JUnitPlatform.class) in the IDE
	// testCompile "org.junit.platform:junit-platform-runner:${junitPlatformVersion}"

    // Only needed to run tests in an IDE that bundles an older version
	// testRuntime("org.junit.platform:junit-platform-launcher:${junitPlatformVersion}")

    // Junit
    testCompile("org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion")
    testCompile('org.junit.platform:junit-platform-runner:1.0.0')
    
    // Mockito
    testCompile('org.mockito:mockito-all:1.8.4')

    // Engines
    testRuntime("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion")
}

 

Changements

  • Le package est org.junit.jupiter.api​
  • Les assertions se trouvent dans org.junit.jupiter.api.Assertions​
  • Les assumptions sont dans org.junit.jupiter.api.Assumptions
  • @Before et @After deviennent respectivement @BeforeEach et @AfterEach​
  • @BeforeClass et @AfterClass deviennent respectivement @BeforeAll et @AfterAll​
  • @Ignore devient @Disabled​
  • @Category devient @Tag
  • @RunWith est remplacé par @ExtendWith​
  • @Rule and @ClassRule sont remplacés par @ExtendWith​

Assertions

En ce qui concerne les assertions, il y a eu un petit changement qui n'est pas anodin mais qui ne change pas grand chose : l'ordre des paramètres des méthodes a changé :

assertEquals(4, 4, "The optional assertion message is now the last parameter.");

Nous retrouvons bien entendu les méthodes d'assert de base de Junit 4 à savoir : assertTrue, assertFalse, fail, assertEquals, assertNotEquals, assertArrayEquals, assertNotNull, assertNull, assertSame, assertNotSame et format.

Ces méthodes sont tout de même enrichies par rapport à la v4 (lambada entre autres).

Les nouvelles méthodes d'assert sont les suivantes : assertAll, assertThrows, assertTimeout, assertTimeoutPreemptively, assertIterableEquals et assertLinesMatch.

Voici deux petits exemples avec assertAll. Pour commencer une assertion groupée :


    @Test
    void groupedAssertions() {
        assertAll("personne",
                () -> assertEquals("Frodo", personne.getFirstName()),
                () -> assertEquals("Bessac", personne.getLastName())
        );
    }

Il y a également la possibilité de faire des assertions dépendantes; le test dépendant n'est exécuté que si le parent réussi ce qui peut être fort pratique si nous souhaitons enchainer des tests entre eux.

assertAll("properties",
                () -> {
                    String firstName = personne.getFirstName();
                    assertNotNull(firstName);

                    // Executed only if the previous assertion is valid.
                    assertAll("first name",
                            () -> assertTrue(firstName.startsWith("F")),
                            () -> assertTrue(firstName.endsWith("o"))
                    );
                },
                () -> {
                    // Grouped assertion, so processed independently
                    // of results of first name assertions.
                    String lastName = personne.getLastName();
                    assertNotNull(lastName);

                    // Executed only if the previous assertion is valid.
                    assertAll("last name",
                            () -> assertTrue(lastName.startsWith("B")),
                            () -> assertTrue(lastName.endsWith("c"))
                    );
                }
        );

 Nouveautés

@DisplayName

L'annotation  permet de "nommer" le test afin qu'il soit plus parlant et qu'ils apparaissent avec ce nom dans l'IDE au niveau des résultats de tests

@Test
    @DisplayName("Custom test name containing spaces")
    void testWithDisplayNameContainingSpaces() {
    }

Il y a toujours la possibilité de venir greffer des librairies de tests comme AssertJ ou Hamcrest.

Il existe un mécanisme d'assumptions qui permet de n’exécuter la suite du test que si une condition est vraie.

@Test
void testOnlyOnCiServer() {
        assumeTrue("CI".equals(System.getenv("ENV")));
        // votre test sur CI
}

@Test
void testInAllEnvironments() {
	assumingThat("CI".equals(System.getenv("ENV")),
				 () -> {
		// seulement sur le CI
		assertEquals(2, 2);
	});

	// sur toutes les plateformes
	assertEquals("a string", "a string");
}

@Nested

Junit 5 introduit une notion de tests imbriqués. Il est donc désormais possible de regrouper des tests au sein d'une même classe.


@DisplayName("A stack")
class NestedTest {
    Stack<Object> stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, () -> stack.pop());
        }

        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, () -> stack.peek());
        }

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

        }
    }
}

@RepeatedTest

Vous avez aussi la notion d'effectuer une répétition qui vous permet de venir exécuter votre test X fois. Il y a la possibilité d'afficher des infos supplémentaires à l'exécution grâce à des paramètres liées à l'annot

    @RepeatedTest(value = 5, name = "Répétition n°{currentRepetition}/{totalRepetitions}")
    void repeatedTest() {
        // ...
    }

Cela donnera ce genre de chose dans votre IDE

@ParameterizedTest

Cette notion vous permet de venir exécuter des tests via des jeux de données. Il est possible grâce à une annot supplémentaire de fournir en entrée des sources pour une méthode de test donnée :

  • un tableau de strings "en dur"
  • source CSV en dur ( @CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" }) )
  • enumeration source ( @EnumSource(value = TimeUnit.class, mode = MATCH_ALL, names = "^(M|N).+SECONDS$") )
  • ...
  • fichier CSV présent dans les resources de tests (cf. ci-dessous)

    @ParameterizedTest(name = "{index} ==> first=''{0}'', second={1}")
    @CsvFileSource(resources = "/two-column.csv")
    void testWithCsvFileSource(String first, int second) {
        assertNotNull(first);
        assertNotEquals(0, second);
    }

Vous obtenez donc ceci dans votre IDE préféré :

Il existe d'autres choses mais je vous laisse le soin de parcourir la doc JUnit 5 pour découvrir les autres nouveautés.

 

Configuration

Junit 5 vient avec la notion de sélecteurs et de filtres. Cela nous permet d'effectuer un paramétrage qui déclenchera des tests de façon différente suivants nos environnements.

Nous pourrons donc ainsi faciliter l'utilisation sur les différents outils et environnements que nous pouvons avoir sur nos projets.

Filtres

Les filtres sont directement intégrés à JunitPlateform et sont donc plus facilement configurables (plus directement en tout cas); nous avons juste à déclarer ce qui suit dans notre conf gradle :

junitPlatform {
...
    filters {
        engines {
            // include 'junit-jupiter', 'junit-vintage'
            // exclude 'custom-engine'
        }
        tags {
            // include 'fast'
            exclude 'slow'
        }
        // includeClassNamePattern '.*Test'
    }
}

Cette configuration aura pour effet d'exclure les tests (classe entière ou méthodes de tests) qui ont le tag 'slow'. Vous avez également la possibilité de venir désactiver les moteurs d’exécution également. Cela peut-être pratique pour n'exécuter que les tests junit 4 ou 5.

Vous avez également la possibilité de venir inclure des classes de tests par pattern matching (utilisé via plugin avant avec maven).

Sélecteurs

 

Grâce aux sélecteurs, vous pouvez par exemple ne sélectionner que les tests qui se trouve dans un package particulier et prendre en plus certaines classes.

Vous pourrez donc lancer des tests différents suivants les environnements sur vos jobs Jenkins, venir faire un job tests unitaires et un job tests d'intégration voire même alléger l’exécution des tests en local.

Extensions

Voici un exemple basique qui ne reflète pas forcement un cas usage réel mais qui nous permet d'assimiler la chose :

@ExtendWith(MockitoExtension.class)
class MockitoTest {

    @Mock Personne personGlobal;

    @BeforeEach
    void init() {
        when(personGlobal.getFirstName()).thenReturn("Legolas");
    }

    @Test
    void simpleTestWithGlobalMock() {
        assertEquals("Legolas", personGlobal.getFirstName());
    }

    @Test
    void simpleTestWithInjectedMock(@Mock Personne personne) {
        when(personne.getFirstName()).thenReturn("Gimli");

        assertEquals("Gimli", personne.getFirstName());
    }
}

Nous constatons que grâce au mécanisme d'extension; dans ce cas précis nous venons initialiser nos mocks globaux mais également nos mocks locaux (injectés dans nos méthodes de tests) via l'extension (MockitoExtension) qui se chargera de tout ça.

Cela permet d'éviter leurs initialisations via une classe abstraite (pour le mock global) ou au sein même de nos tests; notre classe n'est ainsi pas alourdie et plus lisible.

Evolutions de JUnit

  Les classes et les méthodes accessibles sont annotées via @API. La valeur d'utilisation de l'annotation peut être attribuée à l'une des cinq valeurs suivantes :

Cela nous permet donc d'avoir une idée globale de l'évolution des fonctionnalités que l'on utilisera dans nos tests.

Conclusion

Junit 5 apporte quand même une certaine souplesse par rapport au 4 et nous permet de faire des tests plus poussés grâce aux mécanismes qui ont pu être ajoutés. Il est surtout bien plus paramétrable ce qui facilitera nos exécutions de tests sur nos différents environnements.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *