junit-lambda-talk



junit-lambda-talk

1 2


junit-lambda-talk

Source code and slides for my talkt about JUnit Lambda aka JUnit 5. View the slides on http://schauder.github.io/junit-lambda-talk/#/

On Github schauder / junit-lambda-talk

@jensschauderhttp://schauder.github.io/junit-lambda-talk#junit5

JUnit Lambda 5

The Next Generation

Jens Schauderhttp://blog.schauderhaft.de@jensschauderT-Systems on site services GmbH

Wer? ... ich?

Entwickler, Freunde dürfen mich Architekt nennen

JUnit Nutzer seit 2001

JUnit 5 Supporter

Entwickler von Degraph

Warum eine neue Version?

Schwer zu warten / weiter zu entwickeln

Die verdammten Runner

Neue syntaktische Möglichkeiten mit Java 8

Was bisher geschah

2012

2014

Diskussion auf den XP-Days mit den JUnit Entwicklern

Mitte 2015

Indigogo Kampagne

Ziel: 25 000Euro

Ergebnis: 53 937Euro von 474 Unterstützern

Ende 2015

Prototype

2016

JUnit 5.0 Alpha releast

zusammen mit org.opentest4j

JUnit 5 Infrastruktur

Modular

junit-commons
junit-console
junit-engine-api
junit-gradle
junit-launcher
junit4-engine
junit4-runner
junit5-api
junit5-engine
surefire-junit5

Keine Package Zyklen

shameless plug

Running Tests à la JUnit 5

start the launcher

launcher finds available Engines

launcher asks each Engine to discover Tests

launcher asks each Engine execute its Tests

Es gibt eine JUnit 4 Engine die JUnit4 Tests ausführen kann

Keine IDE Integration

Running Tests à la JUnit 4

Anotieren mit @RunWith(JUnit5.class)

JUnit5 Runner startet den Launcher wie oben

Running Tests like a Pro

Gradle Plugin

Maven Plugin

ConsoleRunner

Packages

org.junit.gen5.*

Viele Namen haben sich geändert

Das soll die Verwirrung minimieren. Ob's klappt?

Tests FTW!

JUnit 5 beinhaltet JUnit 4

public class $01JUnit4Test {
    @Test
    public void testShouldFail(){
        Assert.fail("JUnit4 test executed,
                    which is great, so this fails
                    ... whatever");
    }
}

Die Klassennamen haben nichts zu bedeuten

JUnit 5!

import org.junit.gen5.api.Assertions;
import org.junit.gen5.api.Test;

public class $02JUnit5Test {
    @Test
    public void testShouldFail() {
        Assertions.fail("JUnit5 test executed,
                    which is great, so this fails
                    ... whatever");
    }
}

JUnit 5 via JUnit4

import org.junit.gen5.api.Assertions;
import org.junit.gen5.api.Test;
import org.junit.gen5.junit4.runner.JUnit5;
import org.junit.runner.RunWith;

@RunWith(JUnit5.class)
public class $03JUnit5AsJUnit4Test {
    @Test
    public void testShouldFail(){
        Assertions.fail("This is a JUnit 5 Test which uses a Runner,
                    so it can run as a JUnit 5 or as a JUnit 4 Test,
                    which is great, so this fails
                    ... whatever");
    }
}

JUnit5 for real

Wrapper für die nächsten Folien

import org.junit.gen5.api.Assertions;
import org.junit.gen5.api.Assumptions;
import org.junit.gen5.api.Disabled;
import org.junit.gen5.api.DisplayName;
import org.junit.gen5.api.Test;

class $04ProperJUnit5Test {

//  STUFF GOES HERE

}

Package Scope reicht

Trivialer Test

@Test
void shouldFail() { // again only package scope -> reduced noise
    // Assertions replaces Assert for simple stuff
    Assertions.fail("JUnit5 test executed, which is great,
                    so this fails ... whatever");
    // you probably want to use AssertJ, Hamcrest or similar anyway
}

Tests Ingorieren

@Test
@Disabled // replaces @Ignored
void ignored() {
    // does not get executed
}

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

Namen

@Test
@DisplayName("Realy Awesome Name \uD83D\uDC4C")
void stupidName() {}

Exceptions

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

int parseRomanNumeral(String numberString) {
    if (numberString.equals("XXIII")) return 23;

    throw new IllegalArgumentException("Can't parse " + numberString);
}

Extensions

ParameterResolver 1/2

@ExtendWith({RandomParameterResolver.class})
public class $05MethodParameterResolver {
    @Test
    void testMethodParameterResolver(
    String arg,
    TestInfo testInfo // is provided by the TestInfoResolver,
                    // which is always present
    ) {
        assertThat(testInfo.getDisplayName() , endsWith("Resolver"));
        assertThat(arg, startsWith("Jens"));
    }
}

ParameterResolver 2/2

public class RandomParameterResolver implements MethodParameterResolver{
    @Override
    public boolean supports(
        Parameter parameter,
        MethodInvocationContext methodInvocationContext,
        ExtensionContext extensionContext
    ) throws ParameterResolutionException {
        return parameter.getType() == String.class;
    }

    @Override
    public Object resolve(
        Parameter parameter,
        MethodInvocationContext methodInvocationContext,
        ExtensionContext extensionContext
    ) throws ParameterResolutionException {
        return "Jens" + UUID.randomUUID();
    }
}

Extension Points

Condition: disablen von TestsInstancePostProcessor: Testinstanzen manipulierenMethodParameterResolver: Parameter injizierenBeforeEachExtensionPointAfterEachExtensionPointBeforeAllExtensionPointAfterAllExtensionPoint

Before und After bündeln: Test

@ExtendWith({WithDatasource.class})
    public class BeforeAfterExtensionTest implements NeedDatasource{

    @Test
    public void testSomething(){
        System.out.println("in test one");
    }

    @Test
    public void testSomethingElse(){
        System.out.println("in test two");
    }

    @Override
    public void set(MyDatasource ds) {
        System.out.println("set called with " + ds);
    }
}

Before und After bündeln: Extension

public class WithDatasource implements
    BeforeEachExtensionPoint, AfterEachExtensionPoint {

    @Override
    public void beforeEach(TestExtensionContext testExtensionContext) {
        System.out.println("before");
        ((NeedDatasource)testExtensionContext.getTestInstance())
                   .set(new MyDatasource());
    }

    @Override
    public void afterEach(TestExtensionContext testExtensionContext) {
        System.out.println("after");
        ((NeedDatasource)testExtensionContext.getTestInstance())
                   .set(null);
    }
}

Before und After bündeln: Interface

public interface NeedDatasource {
    void set(MyDatasource ds);
}

Before und After bündeln: Output

before
set called with de.schauderhaft.junit.lambda.example.MyDatasource@4501b7af
in test two
after
set called with null
before
set called with de.schauderhaft.junit.lambda.example.MyDatasource@6093dd95
in test one
after
set called with null

Limitierungen

Keine Statement Abstraktion, wie bei Rules

=> Keine Threads, Mehrfachausführung ...

Abhängig von der Engine

Ich habe einen Issue angelegt

Und es gibt einen alternativen Vorschlag

Dynamische Tests

Branch issue58-dynamic-tests

@Dynamic
List<DynamicTest> dynamicTestsFromList() {
    List<DynamicTest> tests = new ArrayList<>();

    tests.add(new DynamicTest(
        "succeedingTest",
        () -> Assertions.assertTrue(true, "succeeding")
    ));

    tests.add(new DynamicTest(
        "failingTest",
        () -> Assertions.assertTrue(false, "failing")
    ));

    return tests;
}

Eine kleine Klasse

public abstract class ClosureTestBase {
    private List<DynamicTest> tests = new ArrayList<>();

    protected void test(String name, Executable test){
        tests.add(new DynamicTest(name, test));
    }


    @Dynamic
    public List<DynamicTest> registeredTests(){
        return tests;
    }
}

Tada!

class MyClosureTest extends ClosureTestBase{{
    test("ein Test ein Test", () -> {Assertions.assertTrue(true);});
}}

Parametrisierte Tests

for (Fixture f : asList(
    f(1, 2, 3), // creates a Fixture instance
    f(20, 30, 50),
    f(-20, 12, -8))
    )
        test(format("%d plus %d is %d", f.summand1, f.summand2, f.sum),
                () -> assertEquals(f.sum, f.summand1 + f.summand2)
        );

Test Parameter injection

testWithDependencies(
    "those properties must contain some magic",
    (Properties p) -> {
        System.out.println("in test");
        assertTrue(p.containsKey("magic.number"));
    }
);

private void testWithDependencies(
    String name,
    Consumer<Properties> testNeedingProperties
) {
    Properties p = new Properties();
    p.put("magic.number", 42);
    super.test(name, () -> testNeedingProperties.accept(p));
}

Setup Teardown

testWithSetupTearDown(
    "test with setup/teardown",
    () -> assertTrue(true)
);

private void testWithSetupTearDown(String name, Executable test) {
    test(
        name,
        () -> {
            try {
                System.out.println("setup");
                test.execute();
            } finally {
                System.out.println("teardown");
            }
        });
}

Diverses

Nested Tests

class ListTest {
   @Nested
   class WhenEmpty {
       @Test
       void canAdd() {
           // ...
       }
   }
}

Tagging

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

    @Test
    void impressiveTest() {
        //
    }

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

Meta Annotationen

class $08TaggedTest {
     @FastTest
     void metaAnnotatedTest() {
         //
     }
}

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

API Annotations

@API(Experimental)

Mit den Ausprägungen

Internal,
Deprecated,
Experimental,
Maintained,
Stable

Ausblick

Final Release für den Herbst geplant

Statementabstraktion in Diskussion

Das gleiche gilt für fast alles andere

d.h. man kann jetzt sehr direkt Einfluss nehmen

Closure basierte Engine wird es zunächst nicht geben

class SomeSpec extends ClosureSpec{{
    test("ein Test", () -> {
        // Testcontent should go here
    });
}}

Closure basierte Tests via Dynamic Tests

Halte ich für extrem wichtig!

Wenn ihr das auch so seht, äußert euch auf Github

Was solltet ihr jetzt tun?

Marc Philipp (Keeper of the Green Bar)

Ich fürchte, die meisten werden auf die finale Version warten und dann meckern…

Lasst uns ihn Lügen strafen

Führt eure existierenden Tests mit JUnit 5 aus

Schreibt neue Tests mit JUnit 5

Schreibt eure Rules/Runner mit JUnit 5 Mitteln

Meckert freundlich, frühzeitig und konstruktiv auf Github

Done

Fragen?

Jens Schauderhttp://blog.schauderhaft.de@jensschauderT-Systems on site services GmbH

JUnit Lambda 5 The Next Generation Jens Schauder http://blog.schauderhaft.de @jensschauder T-Systems on site services GmbH