JUnit 5
a new Major Release

Historie

Sommer 2015

    Start "JUnit Lambda"

    Integration Java 8 Features

 

Crowdfunding campaign

    leads to JUnit 5

 

Release 5.0 : September 2017

Release 5.1 : Februar 2018

Systemarchitektur

Was ist neu?

  • JUnit Platform Launcher API

  • Extensions
  • Repeated Tests
  • Parameterized Tests
  • Dynamic Tests
  • Nested Tests
  • Test Templates

Was ist weg?

  • Runners
  • Rules

Test bleibt Test...

import org.junit.Test;

public class NameDerTestKlasse {

   @Test
   public void irgendeinBeliebigerMethodenname() {
   }

}

Die Annotation hat keine Attribute (expected, timeout)!

import org.junit.jupiter.api.Test;

public class NameDerTestKlasse {

   @Test
   public void irgendeinBeliebigerMethodenname() {
   }

}

Unterschiede

package de.mike.tests;

import org.junit.jupiter.api.Test;

class NameDerTestKlasse {

   @Test
   void irgendeinBeliebigerMethodenname() {
   }

}

Testklassen und ~methoden müssen
nicht mehr public sein.

Executions

  • Maven call (mvn test)
     
  • Console Runner
     
  • JUnitPlatform Runner
     
  • Eclipse (seit Oxygen 4.7.0 20170620-1800)
     
  • IntelliJ Integration

Console Runner

java -jar ${dir}/junit-platform-console-standalone-1.0.0.jar

     --classpath target/test-classes:target/classes

     --include-classname ^.*Demo?$

     --select-class de.mike.training.junit5.FirstTestDemo

Eigenes ausführbares Java Archiv:
 

junit-platform-console-standalone-1.0.0.jar

JUnitPlatform Runner

package de.mike.training.junit5;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;


@RunWith(org.junit.platform.runner.JUnitPlatform.class)
class FirstTest {

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

Eclipse Oxygen

Neue Tests können direkt als JUnit 5 Tests erstellt werden.

Run As... ermöglicht als TestRunner auch JUnit 5 zu...

...und der Output wurde entsprechend angepasst.

Äquivalenzen

JUnit 4 JUnit 5
@Before @BeforeEach
@After @AfterEach
@BeforeClass @BeforeAll
@AfterClass @AfterAll
@Categories @Tag
@Ignore @Disabled

Tagging und Filtering

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

@Tag("fast")
@Tag("junit5-demo")
class TaggingDemo {

    @Test
    @Tag("orders")
    void testingSpecialCalculation() {
    }

}

Maven Konfiguration

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.19.1</version>
    <configuration>
        <argLine>${surefireArgLine}</argLine>
        <properties>
            <includeTags>junit5-demo</includeTags>
            <excludeTags>orders</excludeTags>
        </properties>
    </configuration>
</plugin>

Meta Annotations

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

import org.junit.jupiter.api.Tag;

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("integration")
public @interface IntegrationTest {
}

Kombinationen von Annotationen können in JUnit 5 genutzt werden, um einfachere Bezeichner zu erhalten.

Displayname

@DisplayName("Dies ist ein Spezialtestfall")
class DisplayNameDemo {

   @Test
   @DisplayName("Mein Name mit Leerzeichen 😱")
   void testWhatever() {
      ...
   }
}

Es können nun für Test Cases und Methoden
bessere Bezeichner (inkl. Leerzeichen, Sonderzeichen und Emojis) benutzt werden.

Assertions

  • Timeout
  • Assertions mit Supplier
  • (Un-)abhängige Assertions 

Timeouts

@Test
void timeoutExceeded() {
   // Execution exceeded timeout of 10 ms by e.g. 91 ms
   assertTimeout(ofMillis(10), () -> {  /* here your test */
   });
}
...

Als harter Abbruch...

... oder weiche Prüfung

...
@Test
void timeoutExceededWithPreemptiveTermination() {
   // Execution timed out (at the latest) after 10 ms
   assertTimeoutPreemptively(ofMillis(10), () -> { /* here your test */ 
   });
}

Assertions mit Supplier

@Test
void myFirstTestWithSupplier() {

    assertTrue(1 + 1 == 2, () -> "1 + 1 should equal 2");

    assertAll("Check all assertions and report each", 
         () -> assertTrue(1 + 2 == 5, "1 + 2 should equal 3"),
         () -> assertTrue(1 + 3 == 5, "1 + 3 should equal 4"), 
         () -> assertTrue(2 + 2 == 5, "2 + 2 should equal 4")
    );
}

Mit assertAll können mehrere Assertions geprüft werden, bevor evtl. eine fehlschlägt.

(Un-) abhängige Assertions

@Test
void testMyPerson() {

   assertAll("Failure", 
      () -> {
            assertThat(...); /* If this fails, */
            assertThat(...); /* this will not be checked */
            },
      () -> assertNotNull(...); /* but this anyway */
   }

}

Natürlich können ab- und unabhängige Assertions miteinander kombiniert werden.

Assumptions

import static org.junit.jupiter.api.Assumptions.assumeTrue;

@Test
void testOnlyOnITestServer() {
    assumeTrue("ITest".equals(System.getenv("Staging")));
    ...
}

@Test
void testOnlyOnCTestServer() {
    assumeTrue("CTest".equals(System.getenv("Staging")),
        () -> "Aborting test: not on CTest server");
     ...
}

Werden die Vorannahmen nicht erfüllt,
wird der Test nicht ausgeführt.

assume that...

import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumeThat;

@Test
void testInAllEnvironments() {
    assumingThat("CTest".equals(System.getenv("Staging")),
        () -> {
            // perform these assertions only on CTest server
            assertThat(...);
        });

    // perform these assertions in all environments
    assertThat(...);
}

Werden die Vorannahmen nicht erfüllt,
wird die Assertion nicht ausgeführt.

Test Lifecycle

@TestInstance(Lifecycle.PER_CLASS)
public class TestLifecycleDemo {

   private int index = 0;

   @Test
   public void test1() throws Exception {
      index++;
      System.out.println("Ich bin Objekt " + index + " mit ID <" + this + ">");
   }

   ...
}

Standardmäßig ist der Lifecycle wie bei älteren JUnit-Versionen:

Pro Testmethode wird eine neue Instanz der Testklasse erzeugt.

Dies kann nun durch @TestInstance(Lifecycle.PER_CLASS) geändert werden.

Extensions

@ExtendWith({ 
   BeforeAllCallbackExtensionDemo.class, 
   BeforeEachCallBackExtensionDemo.class,
   ...
   AfterAllCallbackExtensionDemo.class, 
   ExecutionConditionExtensionDemo.class,
   })
public class ExtensionDemo {

Es können Testklassen und ~methoden erweitert werden.

Extensions

Mit dem Konzept der Extensions ermöglicht JUnit5 nun die lückenlose Erweiterung sämtlicher Tests und Durchläufe.

Die rechts stehenden Interfaces bzw. deren Implementierungen dienen als Hook in den allgemeinen Ablauf.

Extensions
Lifecycle

ExecutionCondition:ExtensionDemo
BeforeAllCallback:ExtensionDemo
Ich bin beforeAll.
   ExecutionCondition:ExtensionDemo 1
   BeforeEachCallBack:ExtensionDemo 1
   BeforeEachMethodAdapter:ExtensionDemo 1
   Ich bin beforeEach.
   BeforeTestExecutionCallback:ExtensionDemo 1
   Ich bin der Testlauf 1.
   AfterTestExecutionCallback:ExtensionDemo 1
   Ich bin afterEach
   AfterEachMethodAdapter:ExtensionDemo 1
   AfterEachCallBack:ExtensionDemo 1

   ExecutionCondition:ExtensionDemo 2
   BeforeEachCallBack:ExtensionDemo 2
   BeforeEachMethodAdapter:ExtensionDemo 2
   Ich bin beforeEach.
   BeforeTestExecutionCallback:ExtensionDemo 2
   Ich bin der Testlauf 2.
   AfterTestExecutionCallback:ExtensionDemo 2
   Ich bin afterEach
   AfterEachMethodAdapter:ExtensionDemo 2
   AfterEachCallBack:ExtensionDemo 2
Ich bin afterAll
AfterAllCallback:ExtensionDemo

ExecutionCondition

public interface ExecutionCondition extends Extension {
   ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context);
}

Hook, der die Auswertung einer Bedingung vor Testausführung bewirkt und andernfalls den Test verwirft.

class DisabledCondition implements ExecutionCondition {

	private static final ConditionEvaluationResult ENABLED = ConditionEvaluationResult.enabled(
		"@Disabled is not present");

	@Override
	public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
		Optional<AnnotatedElement> element = context.getElement();
		Optional<Disabled> disabled = findAnnotation(element, Disabled.class);
		if (disabled.isPresent()) {
			String reason = disabled.map(Disabled::value).filter(StringUtils::isNotBlank).orElseGet(
				() -> element.get() + " is @Disabled");
			return ConditionEvaluationResult.disabled(reason);
		}

		return ENABLED;
	}

}

Implementierungsbeispiel

TestInstancePostProcessor

public interface TestInstancePostProcessor extends Extension {

   void postProcessTestInstance(Object testInstance, ExtensionContext context) 
        throws Exception;

}

Hook zur Nachbearbeitung der auszuführenden Testinstanz.

 

Darüber lassen sich beispielsweise Abhängigkeiten auflösen oder benutzerspezifische Initialisierungen vornehmen.

public class MockitoExtension implements TestInstancePostProcessor {

   @Override
   public void postProcessTestInstance(final Object testInstance, final ExtensionContext context) {
      MockitoAnnotations.initMocks(testInstance);
   }

}

Implementierungsbeispiel

Beispiel: TimerExtension

package de.mike.training.junit5.extensions;

---
public class TimerExtension 
             implements BeforeTestExecutionCallback, 
                        AfterTestExecutionCallback {

   @Override
   public void beforeTestExecution(final ExtensionContext context) throws Exception {
      // start stop watch
   }

   @Override
   public void afterTestExecution(final ExtensionContext context) throws Exception {
      // stop stop watch and log Time
   }

}

Stateful Extensions

context.getStore(Namespace.create(getClass(), context));

Um von einer Extension zur nächsten den Context mit Informationen zu füllen, nutzen wir einen Store.

Der Store wird unter einem spezifischen Namen erzeugt und dient als Datenspeicher im Context.

Nested Tests

@DisplayName("A stack")
class TestingAStackDemo {

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

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

Tests können nun in Tests integriert werden;
natürlich als innere Klassen.

Parameter Resolver

Die Klasse

org.junit.jupiter.api.extension.ParameterResolver 

ist eine dynamische Schnittstelle, um Tests via Konstruktor oder Methoden, Parameter zu injizieren.

Repeated Tests

@RepeatedTest(10)
void repeatedTest() {
    // wird 10mal wiederholt.
}

...

Tests können mit Wiederholungsangabe definiert werden.

@RepeatedTest(value = 3, 
              name = "{displayName} {currentRepetition}/{totalRepetitions}")
@DisplayName("Repeat!")
void customDisplayName(TestInfo testInfo) {
    assertEquals(testInfo.getDisplayName(), "Repeat! 1/3");
}
Ausgabe:

Repeat! 1/3
Repeat! 2/3
Repeat! 3/3

Parameterized Tests

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.Assert.assertThat;

public class Arabic2RomanConverterTest {

   @ParameterizedTest(name = "{0} konvertiert in \"{1}\"")
   @CsvSource({ "0, ''", "1, I", "2, II", "4, IV", "5, V", "9, IX", 
                "40, XL", "90, XC", "400, CD", "500, D", "900, CM",
                "1000, M", "2330, MMCCCXXX", "1984, MCMLXXXIV" })
   @DisplayName("Konvertiere arabische Zahl in römische Zeichen")
   void testDifferentValues(final int arabicNumber, final String romanChars) throws Exception {
      assertThat(new Arabic2RomanConverter().convert(arabicNumber), is(romanChars));
   }
}

JUnit Tests können nun einfach parametrisiert werden:

@ParameterizedTest

Daten aus verschiedenen Quellen

  • ValueSource
  • EnumSource
  • MethodSource (Return: Stream, Iterator, Iterable, array of elements)
  • CsvSource
  • CsvFileSource
  • @ArgumentsSource (durch ArgumentProvider)

Konvertierung der Argumente

  • Implizite Konvertierung für Standarddatentypen
  • Explizite Konvertierung(@ConvertWith(...))

Weitere offene Themen

  • Dynamic Tests
  • Test Templates
  • Extension Model

Unterschiedliche Quellen

  • ValueSource
  • EnumSource
  • MethodSource
    (Return: Stream, Iterator, Iterable, array of elements)
  • CsvSource
  • CsvFileSource
  • @ArgumentsSource
    (durch ArgumentProvider)

Konvertierung der Argumente

  • Implizite Konvertierung für Standarddatentypen
  • Explizite Konvertierung (@ConvertWith(...))

TestTemplates

Syntax

public class TestTemplateDemo {

   @TestTemplate
   @ExtendWith(MyTestTemplateInvocationContextProvider.class)
   void testTemplate(...) {
      ...
   }
}

Zur Erstellung eines TestTemplates benötigt man:

  • das Template
    und
  • einen TestTemplateInvocationProvider

TestTemplateInvocationProvider

public class MyTestTemplateInvocationContextProvider
      implements TestTemplateInvocationContextProvider {

   @Override
   public boolean supportsTestTemplate(final ExtensionContext arg0) {
      return ... // true if this TestTemplateInvocationProvider should be used
   }

   @Override
   public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
         final ExtensionContext arg0) {
      return Stream.of(...);
   }
}

Der TTIP ermittelt,

  1. ob dieser TTIP zum Einsatz kommt und
  2. stellt die Aufrufkontexte zur Verfügung, mit denen das Testtemplate aufgerufen werden soll.

supportsTestTemplate

@Override
public boolean supportsTestTemplate(final ExtensionContext arg0) {
   return true;
}

Normalerweise gibt diese Methode einfach true zurück, wenn ein spezifischer TTIP für ein TestTemplate angelegt wurde:

@Override
public boolean supportsTestTemplate(ExtensionContext context) {
   return isAnnotated(context.getTestMethod(), RepeatedTest.class);
}

In manchen Fällen wird die Deklaration durch eine spezifische Annotation erwünscht, wie im Beispiel der RepeatedTests:

provideTestTemplateInvocationContexts

public interface TestTemplateInvocationContext {

   default String getDisplayName(int invocationIndex) {
      return "[" + invocationIndex + "]";
   }

   default List<Extension> getAdditionalExtensions() {
      return emptyList();
   }
}

Hiermit werden die unterschiedlichen Kontexte, unter denen der Test aufgerufen wird, zusammengebastelt:

@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
      final ExtensionContext arg0) {
   return Stream.of(...);
}

Der Standardkontext TestTemplateInvocationContext liefert Implementierungen für

TestTemplateInvocationContext

Beinhaltet Information und Daten für die einzelnen Durchläufe, wie beispielsweise:

  • Parameter für den einzelnen Testdurchlauf
  • Wiederholungsinformation
  • Vorgaben an die Tests
  • erwartete Werte
  • Erweiterungen für den Test
    • spezifische ParameterResolver

...und was gibt es noch?

  • Dynamic Tests
  • Migration Guide von JUnit4 zu JUnit5
  • Einige Bibliotheken, die nachziehen müssen:
    • Pitest
    • Hamcrest
    • JaCoCo (EclEmma & Sonar Code Coverage)

Noch Fragen?

...dann jetzt oder an:

 

Michael Albrecht

m.albrecht@neusta.de

Twitter: Michael_HB

Block 1, 1002

 

Beispielcode unter:

https://bitbucket.org/bitbucket4mike/junit5examples

JUnit 5 - Migrationworkshop

By Michael Albrecht

JUnit 5 - Migrationworkshop

Migration from JUnit 4 to 5

  • 415