JUnit 5
a new Major Release

Historie

Sommer 2015

    Start "JUnit Lambda"

    Integration Java 8 Features

 

Crowdfunding campaign

    leads to JUnit 5

Systemarchitektur

Empty Test (JUnit 5 Style)

package de.mike.tests;

import org.junit.jupiter.api.Test;

public class NameDerTestKlasse {

   @Test
   public void irgendeinBeliebigerMethodenname() {
   }

}

Die Annotation hat keine Attribute (expected, timeout)!

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

Console Runner

java -jar ${dir}/junit-platform-console-standalone-1.0.0-M5.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-M5.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
package de.mike.training.junit5;

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

class TestFictureDemo {

   @BeforeEach
   void init() {
      // preparation for each test
   }

   @Test
   void test1() {
      // test execution 1
   }

   @Test
   void test2() {
      // test execution 2
   }

   @AfterEach
   void tearDown() {
      // tear down for each test
   }

}

Äquivalenzen

JUnit 4 JUnit 5
@Before @BeforeEach
@After @AfterEach
@BeforeClass @BeforeAll
@AfterClass @AfterAll
@Categories @Tag
@Ignore @Disabled
package de.mike.training.junit5;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

class TestFictureDemo {

   @BeforeAll
   static void initAll() {
      // preparation for all tests
   }

   @Test
   void test1() {
      // test execution 1
   }

   @Test
   void test2() {
      // test execution 2
   }

   @AfterAll
   static void tearDownAll() {
      // tear down for all tests
   }

}

Äquivalenzen

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("complete_test")
public class TaggingDemo {

   @Test
   @Tag("single_test")
   public void test1() {
      ...
   }

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

   @Test
   @Tag("single_test")
   @Tag("other_test")
   public void test3() {
      ...
   }

}

Äquivalenzen

JUnit 4 JUnit 5
@Before @BeforeEach
@After @AfterEach
@BeforeClass @BeforeAll
@AfterClass @AfterAll
@Categories @Tag
@Ignore @Disabled
package de.mike.training.junit5;

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

public class IgnoredTestDemo {

   @Disabled("Disable this test.")
   @Test
   public void test() {
      // nothing will be done
   }
}

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.

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.

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>

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.

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(...))

JUnit 5 - New Major Release

By Michael Albrecht

JUnit 5 - New Major Release

A short presentation of the most important features to start

  • 399