JUnit 5: Preview of new possibilities

Przemysław Sobierajski

Introduction

JUnit 5 comes with a bunch of new features. In this article I will briefly describe most of them. It is a continuation of Introduction to JUnit 5 (part I) – quick start guide article.

Tagging and filtering

Test classes and methods can be tagged.

@Tag("IntegrationTest")
class TaggingExample {

    @Test
    @Tag("suite1")
    void test1() {
    }

    @Test
    @Tag("suite2")
    void test2() {
    }
}

Let me note that neither class nor test methods are public. It is no longer require in JUnit 5.

We can filter tests in maven using e.g. popular Surefire plugin. Below configuration will run all tests tagged as “IntegrationTest” excluding those tagged as “suite2”:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.19</version>
    <configuration>
        <properties>
            <includeTags>IntegrationTest</includeTags>
            <excludeTags>suite2</excludeTags>
        </properties>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-surefire-provider</artifactId>
            <version>1.0.2</version>
        </dependency>
    </dependencies>
</plugin>

Nested Tests

With nested tests we can express the relationship among several group of tests.

class NestedTestsExample {

    private SomeStream tested;

    @DisplayName("Set of tests on newly created class")
    class NewSomeStreamTest {

        @BeforeEach
        void setUp() {
            tested = new SomeStream();
        }

        //test methods...

    }
 
    @DisplayName("Set of tests on opened class")
    class OpenedSomeStream {

        @BeforeEach
        void setUp() {
            tested = new SomeStream();
            tested.open();
        }

        //test methods...

    }

    @DisplayName("Set of tests on closed class")
    class ClosedSomeStream {

        @BeforeEach
        void setUp() {
            tested = new SomeStream();
            tested.open();
            tested.close();
        }

        //test methods...

    }
}

Repeated tests

JUnit 5 provides the ability to repeat a test a specified number of times. It may be helpful when test multithreaded or nondeterministic  code.

@RepeatedTest(10)
void repeatedTest() {
    // this test will be automatically repeated 10 times
    // ...
}

Test instance lifecycle

JUnit by default creates a new instance of each test class before executing each test method. To change default behavior you can annotate your test class with @TestInstance(TestInstance.Lifecycle.PER_CLASS). Tested class instance will be created once per test class, when using this mode.

Parameterized tests

Parameterized tests make it possible to run a test multiple times with different arguments. To declare parameterized test just annotate it with @ParameterizedTest instead of @Test and declare a source that will provide arguments for each invocation. To use parameterized tests you need to add following dependency:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.0.2</version>
    <scope>test</scope>
</dependency>

Junit provides a few argument sources:

@ValueSource lets you specify an array of primitive types:

@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void testWithValueSource(int argument) {
    //first invocation: testWithValueSource(1)
    //second invocation: testWithValueSource(2)
    //third invocation: testWithValueSource(3)
}

@CsvSource may be used when you want to express multiple argument lists as comma-separated values:

@ParameterizedTest
@CsvSource({"1, 2, 3", "5, 5, 10"})
void testWithCsvSource(int first, int second, int third) {
    assertEquals(first + second, third);
}

@MethodSource allows you to declare factory method with arguments. It can produce multiple arguments:

@ParameterizedTest
@MethodSource("dataProviderMethod")
void testWithMethodSource(int arg1, int arg2, int result) {
    assertEquals(arg1 + arg2, result);
}

private static Stream<Arguments> dataProviderMethod() {
    return Stream.of(
            Arguments.of(1, 2, 3),
            Arguments.of(5, 5, 10)
    );
}

There are also other kind of argument sources: @ArgumentSource@CsvFileSource or @EnumSource. I encourage you to read official JUnit 5 user guide for more details.

Dependency injection for constructor and methods

In JUnit 5, test constructors and methods are allowed to have parameters. It enables dependency injection for test constructors and methods. ParameterResolver defines the API which is responsible for resolving parameters at runtime. There are three built-in resolvers: TestInfoParameterResolverRepetitionInfoParameterResolver and TestReporterParameterResolver. Each of them allows to resolve different type of injected parameter. The first one supply an instance of TestInfo class. The TestInfo can be used to retrieve information about current test such as associated tag or test’s display name.

@Test
@DisplayName("First scenario")
@Tag("Unit Test")
void testWithTestInfo(TestInfo testInfo) {
    Logger logger = Logger.getLogger("Test example");
    logger.log(INFO,
            String.format("Running: %s", testInfo.getDisplayName()));
}

If a method parameter in a @RepeatedTest@BeforeEach, or @AfterEach method is of type RepetitionInfo, the RepetitionInfoParameterResolver will supply an instance of RepetitionInfo. It can be used to retrieve information about the current repetition or the total number of repetitions for the test.

@RepeatedTest(5)
void repeatedTest(RepetitionInfo repetitionInfo) {
    Logger logger = Logger.getLogger("Test example");
    logger.log(INFO,
            String.format("Running: %s of %s",
                    repetitionInfo.getCurrentRepetition(),
                    repetitionInfo.getTotalRepetitions()));
}

If you want to print some information to stdout, you should use TestReporter.  TestReporterParameterResolver is responsible for supplying TestReporter instance.

@Test
void testWithTestReporter(TestReporter testReporter) {
    testReporter.publishEntry("key", "value");
}

The output of above test on stdout will be:

timestamp = 2017-12-22T22:41:30.293, key = value

Dynamic tests

Each method annotated with @Test is an example of test case. Test cases are fully specified at compile time and there is no possibility to change its behavior at runtime. JUnit 5 introduce a completely new kind of tests: dynamic tests which is generated at runtime by a factory method that is annotated with @TestFactory. Here is description of a test factory from JUnit5 user guide:

In contrast to @Test methods, a @TestFactory method is not itself a test case but rather a factory for test cases. Thus, a dynamic test is the product of a factory. Technically speaking, a @TestFactory method must return a StreamCollectionIterable or Iterator of DynamicNode instances. Instantiable subclasses of DynamicNode are DynamicContainer and DynamicTestDynamicContainer instances are composed of a display name and a list of dynamic child nodes, enabling the creation of arbitrarily nested hierarchies of dynamic nodes. DynamicTest instances will then be executed lazily, enabling dynamic and even non-deterministic generation of test cases.

class DynamicTestExample {

    private Calculator calc;

    @BeforeEach
    void setUp() {
        calc = new Calculator();
    }

    @TestFactory
    Stream<DynamicTest> powTestsFactory() {
        return IntStream.iterate(0, n -> ++n).limit(5)
                .mapToObj(n -> DynamicTest.dynamicTest(
                        "testPowFor" + n,
                        () -> assertEquals(calc.pow(n), n * n)));
    }
}

Above factory will produce stream DynamicTest at runtime. The dynamic test is composed of test display name and Executable functional interface, which will be executed. In practice, powTestsFactory() method will generate and execute 5 tests methods with testPowFor1testPowFor2 etc. names.

It is important to know that @BeforeEach and @AfterEach methods work differently in dynamic test context. They are executed for the test factory method but not for each dynamic test.

Dynamic tests are marked as experimental feature so there might be some changes in a later release.

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami