JUnit 5

The Next Generation

Who? …​ Me?

Entwickler, Freunde dürfen mich Architekt nennen

JUnit Nutzer seit 2001

JUnit 5 Supporter

Entwickler von Degraph

Das haben wir früher auch nicht gebraucht!

Schwer Wartbar

Internes Feld umbenennen?

NOPE!

Die verdammten Runner

Spring Runner? Cool!

Parameterized Runner? Cool!

Parametrisierte Spring Tests? NOPE!

Neu syntaktische Möglichkeiten

Lambdas!

Historie

2012: erster Vorschlag auf Mailing Liste

2014: Diskussion bei XP-Days Hamburg

Mitte 2015: Indigo Kampagne

Ende 2015: Prototyp

Feb 2016: JUnit 5.0 Alpha

Juli 2016: JUnit Jupiter M1 & M2

Internals

Module

diag 6c95a89aee5bd62266e9b318290502d4

Keine Package Zyklen

Dank Degraph

Ablauf

Launcher starten

Launcher findet Engines

Engines finden Tests

Engines führen Tests aus

Tests starten

Gradle Plugin

Maven Plugin

Console Runner

IntelliJ Plugin (von JetBrains)

Einfache Tests!

JUnit4 Test laufen auch mit JUnit5

diag f80981346cd31b98cf65f3ee08e3202a

JUnit5 läuft mit JUnit4

diag f7122222af7970f276db16c93f97cc74
$02JUnit5WithRunnerTest.java
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
public class $02JUnit5WithRunnerTest {
    @Test
    public void someTest() {
        Assertions.assertEquals(9, 3*3, "Msg last");
    }
}

Workaround für Eclipse

Zeichen sparen

$04ProperJUnit5Test.java
import org.junit.jupiter.api.*;

// class needs only package scope -> reduced noise
class $04ProperJUnit5Test {

    // new annotation
    @Test
    void someTest() { // again only package scope -> reduced noise
        // Assertions replaces Assert for simple stuff
        Assertions.assertEquals(5, quersumme(23));

        // you probably want to use AssertJ, Hamcrest or similar anyway
    }

Tests disablen

$04ProperJUnit5Test.java
    @Test
    @Disabled
    // replaces @Ignored
    void ignored() {
    }

Assume

$04ProperJUnit5Test.java
    @Test
    void abortTests() {
        // abort the test -> not failed, but execution stops anyway.
        Assumptions.assumeTrue(false);
    }

Coole Namen

$04ProperJUnit5Test.java
    @Test
    @DisplayName("Realy Awesome Name \uD83D\uDC4C")
    //  should be an ok symbol -------^
    void stupidName() {
    }

Exceptions

$04ProperJUnit5Test.java
    @Test // the new @Test annotation doesn't have an 'expected'
    void testExceptions() {

        final String argument = "forty-two";
        final IllegalArgumentException exception =
                Assertions.expectThrows(IllegalArgumentException.class, () -> {
                    parseRomanNumeral(argument);
                });
        Assertions.assertTrue(exception.getMessage().contains(argument));
    }

Nested (Source)

$05NestedTests.java
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

class $05NestedTests {

    @Nested
    class NestedClass {
        @Test
        void inNested() {}
    }

    @Test
    void inParent() {}

    // Nested does not work here
    // Note that the test is in a static inner class yet not "part" of the containing test
    static class NestedStaticClass {
        @Test
        void inStaticNested() {}
    }
}

Nested (Result)

nested intellij

Tagging

$06TaggedTest.java
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Tags;
import org.junit.jupiter.api.Test;

@Tag("super")
@Tags({@Tag("awesome"), @Tag("cool")})
class $06TaggedTest {

    @Test
    void impressiveTest() {
    }

    @Test
    @Tag("nice")
    void evenBetterTest() {
    }

    @FastTest
    void metaAnnotatedTest() {
    }
}

Tagging Metaannotationen

FastTest.java
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {}

Filtern (Gradle Plugin)

junitPlatform {
    tags {
        include 'fast', 'smoke'
        // exclude 'slow', 'ci'
    }
    includeClassNamePattern '.*Tests'
}

Before + After (Code)

$07BeforeAfterTest.java
    @BeforeAll
    static void beforeAll(){ System.out.println("before all"); }

    @BeforeEach
    void before(){ System.out.println("before"); }

    @Test
    void testOne() { System.out.println("one"); }

    @Test
    void testTwo() { System.out.println("two"); }

    @AfterAll
    static void afterAll(){ System.out.println("after all"); }

    @AfterEach
    void after(){ System.out.println("after"); }

Before + After (Result)

before all
before
one
after
before
two
after
after all

Dynamische Tests

Caffein required
The following uses Java 8 features. You might want to apply caffein now.

Mit Collections

$10DynamicWithCollectionTest.java
class $10DynamicWithCollectionTest {
    private List<Integer> ints = asList(
            Integer.MAX_VALUE, Integer.MIN_VALUE, -1, 0, 1, 2, 10
    );

    @TestFactory
    List<DynamicTest> additionIsCommutative() {
        List<DynamicTest> tests = new ArrayList<>();
        for (Integer i : ints) {
            for (Integer j : ints) {
                tests.add(dynamicTest(
                        i + " + " + j,
                        () -> {
                            assertEquals(i + j, j + i);
                        }));
            }
        }
        return tests;
    }
}

Mit Streams

$11DynamicWithStreamsTest.java
class $11DynamicWithStreamsTest {
    private List<Integer> ints = asList(
            Integer.MAX_VALUE, Integer.MIN_VALUE, -1, 0, 1, 2, 10
    );

    @TestFactory
    Stream<DynamicTest> additionIsCommutative() {
        return ints.stream().flatMap(
                i -> ints.stream().map(
                        j -> dynamicTest(
                                i + " + " + j,
                                () -> assertEquals(i + j, j + i)
                        )
                )
        );
    }
}

Mit Pixie Dust

$12DynamicWithPixyDustTest.java
class $12DynamicWithPixyDustTest extends LambdaBased {{
        List<Integer> ints = asList(Integer.MAX_VALUE, Integer.MIN_VALUE, -1, 0, 1, 2, 10);

        for (Integer i : ints) {
            for (Integer j : ints) {
                test(
                        format("adding integers is commutative (%d, %d)", i, j),
                        () -> {
                            assertEquals(i + j, j + i);
                        });
            }
        }
    }}

Der Pixie Dust

LambdaBased.java
public abstract class LambdaBased {
    private List<DynamicTest> allTests = new ArrayList<>();

    @TestFactory
    List<DynamicTest> allTests(){
        return allTests;
    }

    /** registers a test
     * @param name name of the test
     * @param executable the actual test
     * */
    protected void test(String name, Executable executable){
        allTests.add(DynamicTest.dynamicTest(name, executable));
    }
}

Warnung

Dynamic Test Lifecycle
The execution lifecycle of a dynamic test is quite different than it is for a standard @Test case. Specifically, there are not any lifecycle callbacks for dynamic tests.

Extending Tests

Tests mit Parameter

$20MethodParameterResolverTest.java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith({RandomParameterResolver.class})
class $20MethodParameterResolverTest {
    @Test
    void testMethodParameterResolver(
            String arg,
            // provided by the TestInfoResolver,
            // which is always present
            TestInfo testInfo
    ) {
        assertThat(testInfo.getDisplayName(), containsString("Resolver"));

        assertThat(arg, startsWith("Jens"));
    }
}

Parameter Resolver

RandomParameterResolver.java
import org.junit.jupiter.api.extension.*;

import java.util.UUID;

public class RandomParameterResolver implements ParameterResolver {

    @Override
    public boolean supports(
            ParameterContext parameterContext,
            ExtensionContext extensionContext
    ) throws ParameterResolutionException {
        return parameterContext.getParameter().getType() == String.class;
    }

    @Override
    public Object resolve(
            ParameterContext parameterContext,
            ExtensionContext extensionContext
    ) throws ParameterResolutionException {
        return "Jens" + UUID.randomUUID();
    }
}

Mehrere Extension kombinieren, der Test

$21BeforeAfterExtensionTest.java
@ExtendWith({WithDatasource.class})
public class $21BeforeAfterExtensionTest {

    @Test
    public void testSomething(MyDatasource ds){
        System.out.println("in test one with DS" + ds);
    }

    @Test
    public void testSomethingElse(MyDatasource ds){
        System.out.println("in test two with DS" + ds);
    }

}

Extensions kombinieren (Before / After)

WithDatasource.java
import org.junit.jupiter.api.extension.*;

public class WithDatasource implements
        BeforeEachCallback, AfterEachCallback, ParameterResolver {

    private MyDatasource ds;

    @Override
    public void beforeEach(TestExtensionContext testExtensionContext) {
        System.out.println("before");
        // more setup code goes here
        ds = new MyDatasource();
    }

    @Override
    public void afterEach(TestExtensionContext testExtensionContext) {
        System.out.println("after");
        ds = null;
    }

Extensions kombinieren (Parameter)

WithDatasource.java
    @Override
    public boolean supports(
            ParameterContext parameterContext,
            ExtensionContext extensionContext
    ) throws ParameterResolutionException {
        return parameterContext.getParameter().getType()
                .isAssignableFrom(MyDatasource.class);
    }

    @Override
    public Object resolve(
            ParameterContext parameterContext,
            ExtensionContext extensionContext
    ) throws ParameterResolutionException {
        return ds;
    }
}

Extensions kombinieren (Datasource)

MyDatasource.java
public class MyDatasource {
    private static int counter = 0;

    {counter++;}

    @Override
    public String toString() {
        return "MyDatasource #" + counter;
    }
}

Extensions kombinieren (Ergebnis)

before
in test one with DSMyDatasource #1
after
before
in test two with DSMyDatasource #2
after

Extension Callbacks

AfterAllCallback
AfterEachCallback
AfterTestExecutionCallback
BeforeAllCallback
BeforeEachCallback
BeforeTestExecutionCallback
ContainerExecutionCondition
ParameterResolver
TestExecutionCondition
TestExecutionExceptionHandler
TestInstancePostProcessor

Limitierungen

  • Tests N-Mal ausführen

  • Tests in anderem Thread ausführen (z.B. Swing EDT)

Was vom Vortrag übrig blieb

API Annotations

@API(Experimental)

Mit den Ausprägungen

Internal,
Deprecated
Experimental
Maintained
Stable

Spring Extension

Ist in Arbeit

Zusammenfassung

  • JUnit 5 wird wesentlich aufgeräumter

  • Mächtiger Extension Mechanismus

  • Test mit Closures möglich aber nicht 100% integriert

  • Schaut es euch an und gebt Feedback

Oder soll Marc Recht behalten?

Ich fürchte, die meisten werden auf die finale Version warten und dann meckern…
Keeper of the Green Bar
— Marc Philipp

Fragen?

Kommt nach Vorne.

Ich antworte gerne.

Allen anderen einen schönen Tag.