JUnit 5 in 5 minutes
Sunday, December 18, 2016
JUnit 5 is the younger, but smarter, brother of JUnit 4 and enables the next generation of testing, offering an improved platform for developing unit tests and keeping backwards compatibility with the code written in version 4. Milestone 3 was released in November 2016, so the framework is still being baked. According to the development team, a GA release is planned for Q1 2017. Until then, let's peep through the keyhole and check what's new.
1. Annotations
Most of the annotations have been renamed and moved from org.junit package
to org.junit.juniper.api.
| JUnit 4 | JUnit 5 |
|---|---|
@Before |
@BeforeEach |
@After |
@AfterEach |
@BeforeClass |
@BeforeAll |
@AfterClass |
@AfterAll |
@Ignored |
@Disabled("reason") |
@RunWith |
@ExtendWith |
2. Assertions
The team has added three improvements around the assertions and moved them from org.junit.Assert
package to org.junit.jupiter.api.Assertions
a) The error message to be displayed in case of failed assertion is the last one in the list
assertEquals(expected, actual, "error message");
b) Lambda expressions
assertTrue(false, () -> "Error. Expected true");
The above example is very trivial but the lambda expression is lazily evaluated which saves time and resources in case
of an expensive message construction logic.
c) Multiple assertions
assertAll("Person",
() -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Doe", person.getSurname()),
() -> assertEquals("SW19 5HP", person.getPostCode()));
This addition is very useful because in the previous version of JUnit the execution stopped at the first failure and did not give
a complete overview of the assertions contained in that particular test. In case of error it will display:
AssertionsTest.multipleAssertionsTest:24 Person (2 failures)
expected: <Doe> but was: <Toe>
expected: <SW19 5HP> but was: <NW19 3RU>
3. Tagging and disabling test cases
Unit tests can be tagged and/or disabled using the @Tag and @Disabled annotations
at class or method level
4. Assumptions
Assumptions are used in situations when assertions should be evaluated only if certain criteria are met:
@Test
void shouldEvaluateSeniors() {
assumeTrue(person.getAge > 65);
assertAll("Person",
() -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Doe", person.getSurname()));
}
5. Exceptions
Exception testing has been ignored most of the times, or it was done superficially. The good news is that JUnit 5 enables
easier testing of type and message. This was done in JUnit 4:
@Test(expected = RuntimeException.class)
public void testException() {
target.throw();
}
And this is how it's done now:
@Test
void testException() {
Throwable e = assertThrows(RuntimeException.class, () -> target.throw());
assertEquals("message", e.getMessage());
}
6. Nested tests
JUnit 5 added nested tests to its toolbox in order to express complex relationships among different groups
of tests. Also, it enables a BDD style of developing unit tests by annotating the test methods and classes
with @DisplayName and assigning a description using a 'human-readable' language. The inner
test classes need to be marked with @Nested in order to enable the wiring. The official
documentation offers a very good example.
7. Dynamic testing
Unit tests developed with version 4 of the framework are static test in the sense that they are fully defined at compile time.
The new idea brought in by the latest version, enables a new kind of test - dynamic test - which is generated at runtime by
a factory method annotated with @TestFactory.
The method marked with @TestFactory is not a test method per se, but it's a factory for dynamic tests
and returns a Stream, Collection or Iterable of DynamicTest instances.
The DynamicTest is a @FunctionalInterface which means that the description and implementation
of the test case can be provided as a lambda expression, like below:
@TestFactory
Collection<DynamicTest> generateDynamicTests() {
return Arrays.asList(
dynamicTest("Test status", () -> assertTrue(p.isMarried())),
dynamicTest("Test age", () -> assertEquals(25, p.getAge()))
);
}
8. Data driven testing
Data driven testing allows the execution of multiple iterations of the same test with different input by avoiding
code duplication. This feature is called 'table driven testing' in other frameworks and languages like Go, Spock
or Cucumber. In JUnit 5 one can implement this using a @TestFactory by streaming over a collection
of test data and create a dynamic test for each entry like below:
@TestFactory
Collection<DynamicTest> generateDynamicTests() {
return testData()
.stream()
.map(d -> dynamicTest("Data:" + d, () -> {
/** implementation */
}))
.collect(Collectors.toList());
}
9. Extensions
As the name states, this feature allows the developers to extend their test code in order to add logic
to the tests in a non-invasive manner and to manage the test's lifecycle. This can be implemented with
the so called extension points which come out-of-the-box in JUnit 5. From a technical point of view, the
extension points are interfaces. The implementing class is called an extension and defines the behaviour
of the extension point. Extensions can be registered using the @ExtendWith annotation at
method or class level.
To use the extension points make sure you import the package org.junit.jupiter.api.extension.
| Extension point | Role |
|---|---|
TestInstancePostProcessor |
Define behaviour to post process the test instance. |
ContainerExecutionCondition |
Define the API for conditional container execution |
ParameterResolver |
Define the API for dynamically resolving parameters at runtime |
TestExecutionExceptionHandler |
Define the API for handling exceptions thrown during test execution |
BeforeAllCallback, BeforeEachCallback, AfterAllCallback, AfterEachCallback |
Define APIs for lifecycle management |
A very good example that illustrates the usage of extensions can be found in the official documentation where execution times are measured using extensions.
After all, JUnit 5 is still in it's early days. Probably the best idea would be to give it a go and see if it works for your project and your needs. Otherwise you might direct your attention to other frameworks which are less verbose and more mature, like Spock.
Happy coding!