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
JUnit Platform Launcher API
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() {
}
}
package de.mike.tests;
import org.junit.jupiter.api.Test;
class NameDerTestKlasse {
@Test
void irgendeinBeliebigerMethodenname() {
}
}
Testklassen und ~methoden müssen
nicht mehr public sein.
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
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() {
...
}
...
}
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.
JUnit 4 | JUnit 5 |
---|---|
@Before | @BeforeEach |
@After | @AfterEach |
@BeforeClass | @BeforeAll |
@AfterClass | @AfterAll |
@Categories | @Tag |
@Ignore | @Disabled |
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("fast")
@Tag("junit5-demo")
class TaggingDemo {
@Test
@Tag("orders")
void testingSpecialCalculation() {
}
}
<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>
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("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.
@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 */
});
}
@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.
@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.
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.
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.
@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.
@ExtendWith({
BeforeAllCallbackExtensionDemo.class,
BeforeEachCallBackExtensionDemo.class,
...
AfterAllCallbackExtensionDemo.class,
ExecutionConditionExtensionDemo.class,
})
public class ExtensionDemo {
Es können Testklassen und ~methoden erweitert werden.
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.
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
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;
}
}
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);
}
}
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
}
}
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.
@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.
Die Klasse
org.junit.jupiter.api.extension.ParameterResolver
ist eine dynamische Schnittstelle, um Tests via Konstruktor oder Methoden, Parameter zu injizieren.
@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
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
public class TestTemplateDemo {
@TestTemplate
@ExtendWith(MyTestTemplateInvocationContextProvider.class)
void testTemplate(...) {
...
}
}
Zur Erstellung eines TestTemplates benötigt man:
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,
@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:
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
Beinhaltet Information und Daten für die einzelnen Durchläufe, wie beispielsweise:
...dann jetzt oder an:
Michael Albrecht
m.albrecht@neusta.de
Twitter: Michael_HB
Block 1, 1002
Beispielcode unter:
https://bitbucket.org/bitbucket4mike/junit5examples