JUnit 5 : Tests Imbriqués Dynamiques

(et autres nouveautés)

Benoit Averty - @Kaidjin

Innovation

Junit 4.12

Décembre 2014

Mai 2012

Junit 4.11

Fin 2017

Junit 5

Au moins 1000 frameworks javascript !

Création de Junit - Fin des années 90

Monopole ?

Junit

TestNG

2412

234

Nombre de projets utilisant le framework parmi les 3862 premiers projets sur github

Junit 5

  • Kickstarter en 2015
  • première milestone en Juillet 2016
  • Release au troisième trimestre 2017

Il est temps de songer à migrer !

1   Installation

2   Migration des tests

3   Nouvelles fonctionnalités !

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class GreetingsTest {
    private Greetings greetings;

    @Before
    public void setup() {
        bcs = new Greetings();
    }

    @Test
    public void getMethodShouldReturnHello() {
        final String expected = "Hello!";
        final String actual = greetings.getGreetings();

        Assert.assertEquals(expected, actual);
    }
}
 <dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

Avant

Après

<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.19.1</version>
            <dependencies>
                <dependency>
                    <groupId>org.junit.platform</groupId>
                    <artifactId>junit-platform-surefire-provider</artifactId>
                    <version>1.1.0</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

<dependencies>
    <dependency>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
        <version>5.1.0</version>
    </dependency>
</dependencies>

Premier test Junit 5

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class Junit5GreetingsTest {
    private Greetings bcs;

    @BeforeEach
    public void setup() {
        bcs = new Greetings();
    }

    @Test
    void greetingsMethodShouldReturnHello() {
        final String expected = "Hello!";
        final String actual = greetings.getGreetings();

        Assertions.assertEquals(expected, actual);
    }
}
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.19.1</version>
            <dependencies>
                <!-- ... -->
            </dependencies>
        </plugin>
    </plugins>
</build>

<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.1.0</version>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.1.0</version>
    </dependency>
</dependencies>

Récap

APIs utilisées dans les tests

détecte et lance les tests

Implémentation du TestEngine pour junit 4 (et plus vieux)

Implémentation du TestEngine pour junit 5

API utilisée par les outils (IntelliJ, maven...) pour piloter plateforme/engine

Les nouveautés

  • @DisplayName
  • Tests imbriqués
  • Tests dynamiques
  • Nouveautés sur les assertions
  • Système d'extensions

@DisplayName

@DisplayName("Greetings")
public class Junit5GreetingsTest {
    private Greetings greetings;

    @BeforeEach
    public void setup() { ... }

    @Test
    @DisplayName("getGreetings() should return \"Hello, world!\"")
    void greetingsMethod() { ... }
}

Pas de support dans maven 😞

Tests imbriqués

@DisplayName("Greetings")
public class Junit5GreetingsTest {
    @Nested
    @DisplayName("getGreetings method")
    class GreetingsMethod {
        @Test
        @DisplayName("Should return \"Hello, world!\"")
        public void greetingsMethod() {
        }
    }

    @Nested
    @DisplayName("other method")
    class Other {
        @Test
        @DisplayName("Should contain \"Junit5\"")
        public void testOtherMethod() {
        }
    }
}
public class Junit5GreetingsTest {
    @BeforeEach
    public void setup() { ... }

    @Nested
    class GreetingsMethod {
        @Nested
        class NominalCase {
            @BeforeEach
            void setup() { ... }

            @Test
            void theTest() { ... }
        }

        @Nested
        class InvalidInput{
            @BeforeEach
            void setup() { ... }

            @Test
            void theTest() { ... }
        }
    }
}

Exécutée une fois chacune

Exécutée deux fois

Tests parametrés et dynamiques

Laborieux avec Junit 4

@RunWith(Parameterized.class)
public class GreetingsTest {

    @Parameters
    public static Collection<Object[]> data() throws URISyntaxException, IOException {
        return Files.lines(Paths.get(ClassLoader.getSystemClassLoader().getResource("data.csv").toURI()))
                .map(line -> line.split(";"))
                .collect(Collectors.toList());
    }

    public GreetingsTest(String a, String b, String c) {
        this.a = Integer.parseInt(a);
        this.b = Integer.parseInt(b);
        expected = Integer.parseInt(c);
    }

    private int a;
    private int b;
    private int expected;

    @Test
    public void multiplyTest() {
        Assert.assertEquals(expected, a*b);
    }
}

Incompatible avec autre runner

Constructeur et champs...

💩

@ParameterizedTest

@ParameterizedTest
@CsvFileSource(resources = "data.csv", delimiter = ';')
void testMultiply(long a, long b, long expected) {
    long actual = service.multiply(a, b);
    Assertions.assertEquals(expected, actual);
}

@ParameterizedTest

@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void paramTest(int param) { ... }
@ParameterizedTest
@MethodSource(names = "paramsProvider")
void testWithMultiArgMethodSource(String first, int second) { ... }

static Stream<Arguments> paramsProvider() {
    return Stream.of(
        ObjectArrayArguments.create("foo", 1), 
        ObjectArrayArguments.create("bar", 2)
    );
}

Et aussi @CsvSource, @EnumSource, @ArgumentsSource

@TestFactory

@TestFactory
Stream<DynamicTest> testWithCsv()  {
    return Files.lines(path)
            .map(line -> line.split(";"))
            .map(args -> 
                dynamicTest(args[0]+"*"+args[1]+" should equal "+args[2], 
                () -> {
                    Assertions.assertEquals(service.multiply(args[0],args[1]), args[2]);
                }
            ));
}

Assertions

Enfin des assertions simples pour les exceptions !

Assertions.assertThrows(ArithmeticException.class, () -> service.divide(5, 0));
org.opentest4j.AssertionFailedError: Expected java.lang.ArithmeticException to be thrown, 
but nothing was thrown.

Assertions

Assertions multiples

Assertions.assertAll("Customer doesn't have the right specs",
            () -> assertEquals(customer.getId(), 123L, "Incorrect ID"),
            () -> assertEquals(customer.getName(), "JohnDoe", "Incorrect Name")
        );
org.opentest4j.MultipleFailuresError: Customer doesn't have the right specs (2 failures)
	Incorrect ID ==> expected: <1234> but was: <123>
	Incorrect name ==> expected: <John Doe> but was: <JohnDoe>

Meilleur modèle d'extensions

@ExtendWith(MockitoExtension.class)
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class SpringAndMockitoIntegrationTest {

    @Mock
    private ArithmeticsService service;

    @Test
    public void aTest() {
        ...
    }
}

Deux extensions sur une même classe ! 🎉

Remplace le concept de "Runner"

Conclusion

  • Pas révolutionnaire
  • Beaucoup de choses rendues plus faciles / plus agréables
  • Migration facile et peu coûteuse

Il est temps de songer à migrer !

Merci !

JUnit 5 : Tests Imbriqués Dynamiques

By Benoit Averty

JUnit 5 : Tests Imbriqués Dynamiques

  • 1,266