Extend JUnit Part 2: JUnit 5
JUnit 5 is out since a few months and starts to be integrated in an usable fashion in main IDE and build tools. In other words it means you can start using it. When you start to look at JUnit 5, you quickly realise it is not a JUnit 4 + 1 but a new solution. So why migrating since it is not yet supported by most framework? Yes, Arquillian for instance, doesn't natively support it even if mainstream :(. However Spring or Meecrowave have native integrations with JUnit 5.
The API is different, even the @Test uses another package but the new programming model brings a new way to extend the execution. We saw in previous part how to use rules to add custom logic in the test execution and start a server or setup the test, now you can write extensions! The composability will be way more straight forward and natural than it was before, proposing a nicer experience to developers.
Without entering into the details because it is not the purpose of the post, the tag support, the native display name support, the test instance configuration, the repeated tests etc... are other features you can be interested in to organise your tests, make better reports or simplify your test logic and which can justify to move to this new version.
Before starting to deal with the extensions, you need to use the new dependency:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.0.2</version>
</dependency>
The surefire configuration also to customize and take care you can't use surefire 2.20.x. To support JUnit 5 you need to add the JUnit 5 surefire provider and you can also support the JUnit 4 adding its integartion with the JUnit5 provider. It can sound complicated but concretely you just need to define your surefire plugin like that - there is the equivalent configuration for gradle:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19.1</version>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-surefire-provider</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>4.12.2</version>
</dependency>
</dependencies>
</plugin>
Once done, you can write your first test:
import org.junit.jupiter.api.Test;
public class JUnit5Test {
@Test
public void run() {
System.out.println("Hello JUnit 5");
}
}
It looks exactly like before....except the import which uses the jupiter (JUnit 5) package.
JUnit 5 Extensions
The extensions are based on a few callbacks you can implement in a class:
- BeforeAllCallback/AfterAllCallback: equivalent to a ClassRule for JUnit 4, it will be executed before/after all tests
- BeforeEachCallback/AfterEachCallback: it will be executed before/after each test, including the potential @BeforeEach/@AfterEach callbacks of the test (equivalent to @Before/@After in JUnit 4)
- BeforeTestExecutionCallback/AfterTestExecutionCallback: equivalent to a Rule for JUnit 4, it will be executed before/after each test
All the extensions defines an extension point - and a single one since it is functional interfaces. A single class can implement them all or not, it is up to you:
public class MyFirstJUnit5Extension implements
BeforeAllCallback, AfterAllCallback,
BeforeEachCallback, AfterEachCallback,
BeforeTestExecutionCallback, AfterTestExecutionCallback {
@Override
public void beforeAll(final ExtensionContext context) throws Exception {
log("beforeAll", context);
}
@Override
public void beforeEach(final ExtensionContext context) throws Exception {
log("beforeEach", context);
}
@Override
public void beforeTestExecution(final ExtensionContext context) throws Exception {
log("beforeTestExecution", context);
}
@Override
public void afterTestExecution(final ExtensionContext context) throws Exception {
log("afterTestExecution", context);
}
@Override
public void afterEach(final ExtensionContext context) throws Exception {
log("afterEach", context);
}
@Override
public void afterAll(final ExtensionContext context) throws Exception {
log("afterAll", context);
}
private void log(final String what, final ExtensionContext context) {
System.out.println(what + ": " + context.getDisplayName() + ", test=" + context.getTestInstance());
}
}
If you run a test with two test methods logging "Test #n", you will have this output:
beforeAll: JUnit5Test, test=Optional.empty
beforeEach: test1(), test=Optional[com.github.rmannibucau.blog.junit.JUnit5Test@7cd62f43]
beforeTestExecution: test1(), test=Optional[com.github.rmannibucau.blog.junit.JUnit5Test@7cd62f43]
Test #1
afterTestExecution: test1(), test=Optional[com.github.rmannibucau.blog.junit.JUnit5Test@7cd62f43]
afterEach: test1(), test=Optional[com.github.rmannibucau.blog.junit.JUnit5Test@7cd62f43]
beforeEach: test2(), test=Optional[com.github.rmannibucau.blog.junit.JUnit5Test@464bee09]
beforeTestExecution: test2(), test=Optional[com.github.rmannibucau.blog.junit.JUnit5Test@464bee09]
Test #2
afterTestExecution: test2(), test=Optional[com.github.rmannibucau.blog.junit.JUnit5Test@464bee09]
afterEach: test2(), test=Optional[com.github.rmannibucau.blog.junit.JUnit5Test@464bee09]
afterAll: JUnit5Test, test=Optional.empty
It allows us to validate the lifecycle and to see that the "all" hooks don't have access to the test instance. This is something important as we'll see very soon.
Before seeing how to impelment the extension properly, we need to see how to activate it. In JUnit 4 we had @ClassRule and @Rule but what do we use in JUnit 5? What you want! Concretely you define the annotation you want - most of the time you will define one holding the extension configuration if needed, then you decorate your annotation with @ExtendWith and pass your extension as parameter - it needs to have a no-arg constructor. Here is a sample:
import org.junit.jupiter.api.extension.ExtendWith;
@Target(TYPE)
@Retention(RUNTIME)
@ExtendWith(MyFirstJUnit5Extension.class)
public @interface MyJunit5 {
// if you need some extension config put it here
}
Then you can decorate your test or test class with that extension:
@MyJunit5
public class JUnit5Test {
@Test
public void test1() {
System.out.println("Test #1");
}
@Test
public void test2() {
System.out.println("Test #2");
}
}
No Junit API except to mark the test class :). Friendly no?
Important: if you make the annotation targetting METHOD as well (or only), then you can define any "all" hook, it wouldn't fail but they would be ignored since the scope would be the method already.
In real life it can look like:
@MeecrowaveConfig
public class JUnit5Test {
@Test
@CleanDatabase(AFTER)
public void test1() {
System.out.println("Test #1");
}
@Test
@SeedDatabase("myscript.sql")
@CleanDatabase(BEFORE_AND_AFTER)
public void test2() {
System.out.println("Test #2");
}
}
This test would start the CDI/JAX-RS server Apache Meecrowave and provision/clean a database for each method. Nice no?
But wait, what about the ordering? There are two ways to order extensions with this API:
- Define your own annotation (@MyTestSetup) and list in order the extensions since @ExtendWith takes multiple classes
- Put the annotations in the order you want the execution respects since the annotations are sorted in Java bytecode so @Server @Database would first execute the server extension(s) and then the database one(s)
Now we know all the hooks and how to use it let's see how to implement an extension. The first thing to know is that the context passed to each hook is hierarchic. This means when you decorate a method (all but "all" hooks), you can access the data of the class contexts if current one doesn't contain it.
Each context has a global storage which is keyed by a namespace. I would recommand you extension to define a custom namespace based on the extension qualified name to avoid conflicts. This can be done this way:
private static final ExtensionContext.Namespace NAMESPACE =
ExtensionContext.Namespace.create(MyFirstJUnit5Extension.class.getName());
Then, you can access a store from this namespace in the context:
final ExtensionContext.Store store = context.getStore(NAMESPACE);
This store is a kind of map where you can put and retrieve informations. If you read the previous post on JUnit 4, it is the way we will replace our rule state. We will put some data into the store in before hooks and clean the state in the corresponding after hook:
public class MyFirstJUnit5Extension implements
BeforeAllCallback, AfterAllCallback {
private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(MyFirstJUnit5Extension.class.getName());
@Override
public void beforeAll(final ExtensionContext context) throws Exception {
final ExtensionContext.Store store = context.getStore(NAMESPACE);
store.put(State.class.getName(), doStart());
}
@Override
public void afterAll(final ExtensionContext context) throws Exception {
State.class.cast(
context.getStore(NAMESPACE).get(State.class.getName())
).stop();
}
private State doStart() {
// ...
}
}
Of course you can define a state per type of callback (all, each, test execution). The doStart() can for instance start a server and the stop() will stop it.
Now we have our hooks and know to organize our code, we need to know how to interact with the test. With JUnit 4 we were able to get feedback from the rule directly. The common example being to access the port of the started server by the rule. Here we don't have the extension instance so how to do? A nice way to solve that is to use the extension itself to inject into the test the expected value.
There are a lot of ways to do it but here is a proposal:
- Define an @Injected annotation - not @Inject which exist everywhere. If you prefer you can do something like @MyExtensionInject:
@Target(FIELD)
@Retention(RUNTIME)
public @interface MyExtensionInject {
}
- Then add in the BeforeEachCallback an implementation which will inject into the test instance the right value. There are multiple ways to do the value matching, the simplest is per type if not ambiguous. If you have multiple values of the same type, just ensure you have a kind of qualifier to distinguish them. A String value() into the inject annotation can make it easy to solve:
@Override
public void beforeEach(final ExtensionContext context) throws Exception {
Class<?> current = context.getRequiredTestClass();
while (current != Object.class) {
Stream.of(current.getDeclaredFields())
.filter(f -> f.isAnnotationPresent(MyExtensionInject.class))
.forEach(f -> {
if (!f.isAccessible()) {
f.setAccessible(true);
}
try {
f.set(context.getRequiredTestInstance(), findInstance(context, f));
} catch (final IllegalAccessException e) {
throw new IllegalArgumentException(e);
}
});
current = current.getSuperclass();
}
}
This just loops over the fields of the test class and if there is an inject annotation set the field to the right value.
Tip: a simpler implementation could implement TestInstancePostProcessor, its usage depends your extension lifecycle. If you want to go further you can even support parameter injection through ParameterResolver API.
In terms of usages, our test can look like:
@MyJunit5
public class JUnit5Test {
@MyExtensionInject
private MyInjection inject;
@Test
public void test1() {
assertNotNull(inject);
}
}
The last missing piece for that code to work is the findInstance() logic. It is highly dependent on your implementation but here is a skeleton implementation to help you to get started:
private Object findInstance(final ExtensionContext context, final Field field) {
final Class<?> type = field.getType();
{
final Object instance = context.getStore(NAMESPACE).get(type);
if (instance != null) {
return instance;
}
}
throw new IllegalArgumentException("No value for " + field);
}
This implementation will just check there is a matching instance in the store associated to this extension. This means if you put your extension instance in a context before this method is called - like in the beforeAll hook - you will be able to inject your extension into the test class. Here is a skeleton of an extension doing that:
public class MyFirstJUnit5Extension implements
BeforeAllCallback, AfterAllCallback, BeforeEachCallback {
private static final ExtensionContext.Namespace NAMESPACE =
ExtensionContext.Namespace.create(MyFirstJUnit5Extension.class.getName());
@Override
public void beforeAll(final ExtensionContext context) throws Exception {
final ExtensionContext.Store store = context.getStore(NAMESPACE);
store.put(State.class.getName(), doStart());
store.put(getClass(), this);
}
@Override
public void afterAll(final ExtensionContext context) throws Exception {
State.class.cast(context.getStore(NAMESPACE).get(State.class.getName())).stop();
}
@Override
public void beforeEach(final ExtensionContext context) throws Exception {
Class<?> current = context.getRequiredTestClass();
while (current != Object.class) {
Stream.of(current.getDeclaredFields())
.filter(f -> f.isAnnotationPresent(MyExtensionInject.class))
.forEach(f -> {
if (!f.isAccessible()) {
f.setAccessible(true);
}
try {
f.set(context.getRequiredTestInstance(), findInstance(context, f));
} catch (final IllegalAccessException e) {
throw new IllegalArgumentException(e);
}
});
current = current.getSuperclass();
}
}
private State doStart() {
// start a server etc
return new State() {
@Override
protected void stop() {
// stop the server
}
};
}
private Object findInstance(final ExtensionContext context, final Field field) {
final Class<?> type = field.getType();
{
final Object instance = context.getStore(NAMESPACE).get(type);
if (instance != null) {
return instance;
}
}
throw new IllegalArgumentException("No value for " + field);
}
private static class State {
protected void stop() {
throw new UnsupportedOperationException("must be implemented inline");
}
}
}
Of course you will need to implement doStart()/stop() but the skeleton is functional and associated with an annotation it allows to use it in a test exactly like that:
@MyJunit5
public class JUnit5Test {
@MyExtensionInject
private MyFirstJUnit5Extension inject;
@Test
public void test() {
assertNotNull(inject);
}
}
The test() method will be executed with the extension context (server started if you respect the comments for the start/stop logic) and with the injection available :).
Elegant no?
Happy testing!
From the same author:
In the same category: