This tutorial explains how to use and create JUnit 5 extensions to modify the behavior of test methods and test classes.
See JUnit tutorial for an introduction to JUnit 5.
1. Using JUnit 5 extension
JUnit 5 extensions allow to extend the behavior of test classes and methods.
These extensions are typically used for:
-
adding additional information to test methods
-
resolving parameters
JUnit 5 extensions unifies the rules and runners concept of JUnit 4. |
To implement an extension you have to implement of of the following interfaces:
-
TestInstancePostProcessor - executed after the test has been created, can be used to inject dependencies into the test instance
-
ExecutionCondition - used to disable tests based on conditions
-
ParameterResolver - used to resolve parameters for test methods
-
TestExecutionExceptionHandler - used to handle exceptions
You can also add lifecycle callbacks: * BeforeAllCallback / AfterAllCallback - executed before / after all test methods in the test class * BeforeEachCallBack / AfterEachCallback – executed before / after each test method
To use any of your custom extensions, you can annotate the test with the @ExtendsWith
annotation.
It is also possible to register an extension to all your tests via a configuration file and a parameter.
See official documentation on how to do that.
2. Exercise: Creating an JUnit 5 ParameterResolver extension
This exercise assumes you have created a project named com.vogella.unittest
and already configured Maven or Gradle to use JUnit 5.
Your parameter extension will allow your test to retrieve String in your methods as parameter.
2.1. Creating the extension
Create a packaged named com.vogella.unittest.extension
.
For this, create the following extension:
package com.vogella.unittest.extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
public class ExtensionVogellaStringParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) (1)
throws ParameterResolutionException {
return (parameterContext.getParameter().getType().equals(String.class));
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
// This could evaluate test input based on external parameters
return "Demo data"; (2)
}
}
1 | Defines the condition for which the parameter resolver is active |
2 | Resolves the parameter, here we simplify return a fixed value but we could use the parameterContext and extensionContext to check |
2.2. Use the parameter extension
Now can you use it in your unit tests.
package com.vogella.unittest.extension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(ExtensionVogellaStringParameterResolver.class)
class ParameterResolverTest {
@Test
void ensureThatJUnit5ExtensionWorks(String parameter) {
assertNotNull(parameter);
assertEquals("Demo data", parameter);
}
}
2.3. Support the @Named annotation for specifying your input
The above implementation is very simple, it allows to inject a fixed string. Lets make this more flexible. We will reused the @Named annotation from JSR 330 to allow to specify the parameter to be injected.
Add a dependency the javax.inject JAR to your project.
For example for Gradle, add the following to your build.gradle file,
implementation 'javax.inject:javax.inject:1'
Use @Named in your test code and extend your parameter extension to be only reposible for a parameter if the annotation is present
Can your parameter extension to use the @Named annotation.
package com.vogella.unittest.extension;
import java.util.Optional;
import jakarta.inject.Named;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
public class ExtensionVogellaStringParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) (1)
throws ParameterResolutionException {
return (parameterContext.isAnnotated(Named.class)
&& parameterContext.getParameter().getType().equals(String.class));
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
boolean annotated = parameterContext.isAnnotated(Named.class);
if (annotated) {
Optional<Named> findAnnotation = parameterContext.findAnnotation(Named.class);
Named named = findAnnotation.get();
if (named.value() != "") {
return named.value();
}
}
return "Not available";
}
}
Now you can use it in your test.
package com.vogella.unittest.extension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import jakarta.inject.Named;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(ExtensionVogellaStringParameterResolver.class)
class ParameterResolverTest {
@Test
void ensureThatJUnit5ExtensionWorks(@Named("super") String parameter) {
assertNotNull(parameter);
assertEquals("super", parameter);
}
@Test
void ensureThatJUnit5ExtensionWorks2(@Named String parameter) {
assertNotNull(parameter);
assertEquals("Not available", parameter);
}
}
3. Exercise: Creating an JUnit 5 life cycle extension
This exercise assumes you have created a project named com.vogella.unittest
and already configured Maven or Gradle to use JUnit 5.
In this exercise you will implement a lifecycle extension which provides the start and end time of each unit test.
3.1. Implement the lifecycle extension
Implement a Java class named ExtensionVogellaLifeCycle
implementing the BeforeEachCallback`and `AfterEachCallback
interfaces.
Solution
package com.vogella.unittest.extension;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
public class ExtensionVogellaLifeCycle implements BeforeEachCallback, AfterEachCallback {
private long startTime;
@Override
public void beforeEach(ExtensionContext context) throws Exception {
startTime = System.currentTimeMillis();
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
System.out.println(context.getDisplayName() + " took " + (System.currentTimeMillis() - startTime) + " ms.");
}
}
3.2. Use your JUnit5 life cycle extension
package com.vogella.unittest.extension;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(ExtensionVogellaLifeCycle.class)
class LifeCycleExtensionTest {
@Test
void test1() {
assertTrue(true);
}
@Test
void test2() {
assertTrue(true);
}
@Test
void test3() {
assertTrue(true);
}
}
Run the test and check the console for the output.
Looks like JUnit5 assigns the initialization time to the first test so it is reported as running longer. |
4. Exercise: Creating an JUnit 5 extension to ignore IOException
This exercise assumes you have created a project named com.vogella.unittest
and already configured Maven or Gradle to use JUnit 5.
In this exercise you create an extension with allows to ignore IOExceptions
4.1. Creating a extension
Implement a public class IgnoreIOExceptionExtension implements TestExecutionExceptionHandler
which allows to ignore IOException.
Solution
package com.vogella.unittest.extension;
import java.io.IOException;
import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
public class IgnoreIOExceptionExtension implements TestExecutionExceptionHandler {
@Override public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable {
if (throwable instanceof IOException) { return; } throw throwable; } }
4.2. Write a test using it
Create a new test class which uses this extension. Throw a new IOException in a new test and ensure that the test fails if the extension is not used and is successful if used.
Solution
package com.vogella.unittest.extension;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir;
@ExtendWith(IgnoreIOExceptionExtension.class) class TestIgnoringIOExceptions { @Test void testThatIOExceptionsAreIgnored(@TempDir Path path) throws IOException { Files.writeString(path, "Hello", StandardOpenOption.APPEND); assertTrue(true); } }
5. JUnit Resources
5.1. vogella Java example code
If you need more assistance we offer Online Training and Onsite training as well as consulting