JUnit 5: Preview of new possibilities
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: TestInfoParameterResolver
, RepetitionInfoParameterResolver
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 aStream
,Collection
,Iterable
orIterator
ofDynamicNode
instances. Instantiable subclasses ofDynamicNode
areDynamicContainer
andDynamicTest
.DynamicContainer
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 testPowFor1
, testPowFor2
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.