Master the most popular testing framework for Java, through the Learn JUnit course:
Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
1. Overview
In this article, weβre going to take a look at the extension model in the JUnit 5 testing library. As the name suggests, the purpose of Junit 5 extensions is to extend the behavior of test classes or methods, and these can be reused for multiple tests.
Before Junit 5, the JUnit 4 version of the library used two types of components for extending a test: test runners and rules. By comparison, JUnit 5 simplifies the extension mechanism by introducing a single concept: the Extension API.
2. JUnit 5 Extension Model
JUnit 5 extensions are related to a certain event in the execution of a test, referred to as an extension point. When a certain life cycle phase is reached, the JUnit engine calls registered extensions.
Five main types of extension points can be used:
- test instance post-processing
- conditional test execution
- life-cycle callbacks
- parameter resolution
- exception handling
Weβll go through each of these in more detail in the following sections.
3. Maven Dependencies
First, letβs add the project dependencies we will need for our examples. The main JUnit 5 library weβll need is junit-jupiter-engine:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.11.0-M2</version>
<scope>test</scope>
</dependency>
Also, letβs also add two helper libraries to use for our examples:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.196</version>
</dependency>
The latest versions of junit-jupiter-engine, h2 and log4j-core can be downloaded from Maven Central.
4. Creating JUnit 5 Extensions
To create a JUnit 5 extension, we need to define a class which implements one or more interfaces corresponding to the JUnit 5 extension points. All of these interfaces extend the main Extension interface, which is only a marker interface.
4.1. TestInstancePostProcessor Extension
This type of extension is executed after an instance of a test has been created. The interface to implement is TestInstancePostProcessor which has a postProcessTestInstance() method to override.
A typical use case for this extension is injecting dependencies into the instance. For example, letβs create an extension which instantiates a logger object, then calls the setLogger() method on the test instance:
public class LoggingExtension implements TestInstancePostProcessor {
@Override
public void postProcessTestInstance(Object testInstance,
ExtensionContext context) throws Exception {
Logger logger = LogManager.getLogger(testInstance.getClass());
testInstance.getClass()
.getMethod("setLogger", Logger.class)
.invoke(testInstance, logger);
}
}
As can be seen above, the postProcessTestInstance() method provides access to the test instance and calls the setLogger() method of the test class using the mechanism of reflection.
4.2. Conditional Test Execution
JUnit 5 provides a type of extension that can control whether or not a test should be run. This is defined by implementing the ExecutionCondition interface.
Letβs create an EnvironmentExtension class which implements this interface and overrides the evaluateExecutionCondition() method.
The method verifies if a property representing the current environment name equals βqaβ and disables the test in this case:
public class EnvironmentExtension implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluateExecutionCondition(
ExtensionContext context) {
Properties props = new Properties();
props.load(EnvironmentExtension.class
.getResourceAsStream("application.properties"));
String env = props.getProperty("env");
if ("qa".equalsIgnoreCase(env)) {
return ConditionEvaluationResult
.disabled("Test disabled on QA environment");
}
return ConditionEvaluationResult.enabled(
"Test enabled on QA environment");
}
}
As a result, tests that register this extension will not be run on the βqaβ environment.
If we do not want a condition to be validated, we can deactivate it by setting the junit.conditions.deactivate configuration key to a pattern that matches the condition.
This can be achieved by starting the JVM with the -Djunit.conditions.deactivate=<pattern> property, or by adding a configuration parameter to the LauncherDiscoveryRequest:
public class TestLauncher {
public static void main(String[] args) {
LauncherDiscoveryRequest request
= LauncherDiscoveryRequestBuilder.request()
.selectors(selectClass("com.baeldung.EmployeesTest"))
.configurationParameter(
"junit.conditions.deactivate",
"com.baeldung.extensions.*")
.build();
TestPlan plan = LauncherFactory.create().discover(request);
Launcher launcher = LauncherFactory.create();
SummaryGeneratingListener summaryGeneratingListener
= new SummaryGeneratingListener();
launcher.execute(
request,
new TestExecutionListener[] { summaryGeneratingListener });
System.out.println(summaryGeneratingListener.getSummary());
}
}
4.3. Lifecycle Callbacks
This set of extensions is related to events in a testβs lifecycle and can be defined by implementing the following interfaces:
- BeforeAllCallback and AfterAllCallback β executed before and after all the test methods are executed
- BeforeEachCallBack and AfterEachCallback β executed before and after each test method
- BeforeTestExecutionCallback and AfterTestExecutionCallback β executed immediately before and immediately after a test method
If the test also defines its lifecycle methods, the order of execution is:
- BeforeAllCallback
- BeforeAll
- BeforeEachCallback
- BeforeEach
- BeforeTestExecutionCallback
- Test
- AfterTestExecutionCallback
- AfterEach
- AfterEachCallback
- AfterAll
- AfterAllCallback
For our example, letβs define a class which implements some of these interfaces and controls the behavior of a test that accesses a database using JDBC.
First, letβs create a simple Employee entity:
public class Employee {
private long id;
private String firstName;
// constructors, getters, setters
}
We will also need a utility class that creates a Connection based on a .properties file:
public class JdbcConnectionUtil {
private static Connection con;
public static Connection getConnection()
throws IOException, ClassNotFoundException, SQLException{
if (con == null) {
// create connection
return con;
}
return con;
}
}
Finally, letβs add a simple JDBC-based DAO that manipulates Employee records:
public class EmployeeJdbcDao {
private Connection con;
public EmployeeJdbcDao(Connection con) {
this.con = con;
}
public void createTable() throws SQLException {
// create employees table
}
public void add(Employee emp) throws SQLException {
// add employee record
}
public List<Employee> findAll() throws SQLException {
// query all employee records
}
}
Letβs create our extension which implements some of the lifecycle interfaces:
public class EmployeeDatabaseSetupExtension implements
BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback {
//...
}
Each of these interfaces contains a method we need to override.
For the BeforeAllCallback interface, we will override the beforeAll() method and add the logic to create our employees table before any test method is executed:
private EmployeeJdbcDao employeeDao = new EmployeeJdbcDao();
@Override
public void beforeAll(ExtensionContext context) throws SQLException {
employeeDao.createTable();
}
Next, we will make use of the BeforeEachCallback and AfterEachCallback to wrap each test method in a transaction. The purpose of this is to roll back any changes to the database executed in the test method so that the next test will run on a clean database.
In the beforeEach() method, we will create a save point to use for rolling back the state of the database to:
private Connection con = JdbcConnectionUtil.getConnection();
private Savepoint savepoint;
@Override
public void beforeEach(ExtensionContext context) throws SQLException {
con.setAutoCommit(false);
savepoint = con.setSavepoint("before");
}
Then, in the afterEach() method, weβll roll back the database changes made during the execution of a test method:
@Override
public void afterEach(ExtensionContext context) throws SQLException {
con.rollback(savepoint);
}
To close the connection, weβll make use of the afterAll() method, executed after all the tests have finished:
@Override
public void afterAll(ExtensionContext context) throws SQLException {
if (con != null) {
con.close();
}
}
4.4. Parameter Resolution
If a test constructor or method receives a parameter, this must be resolved at runtime by a ParameterResolver.
Letβs define our own custom ParameterResolver that resolves parameters of type EmployeeJdbcDao:
public class EmployeeDaoParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType()
.equals(EmployeeJdbcDao.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return new EmployeeJdbcDao();
}
}
Our resolver implements the ParameterResolver interface and overrides the supportsParameter() and resolveParameter() methods. The first of these verify the type of the parameter, while the second defines the logic to obtain a parameter instance.
4.5. Exception Handling
Last but not least, the TestExecutionExceptionHandler interface can be used to define the behavior of a test when encountering certain types of exceptions.
For example, we can create an extension which will log and ignore all exceptions of type FileNotFoundException, while rethrowing any other type:
public class IgnoreFileNotFoundExceptionExtension
implements TestExecutionExceptionHandler {
Logger logger = LogManager
.getLogger(IgnoreFileNotFoundExceptionExtension.class);
@Override
public void handleTestExecutionException(ExtensionContext context,
Throwable throwable) throws Throwable {
if (throwable instanceof FileNotFoundException) {
logger.error("File not found:" + throwable.getMessage());
return;
}
throw throwable;
}
}
5. Registering Extensions
Now that we have defined our test extensions, we need to register them with a JUnit 5 test. To achieve this, we can make use of the @ExtendWith annotation.
The annotation can be added multiple time to a test, or receive a list of extensions as a parameter:
@ExtendWith({ EnvironmentExtension.class,
EmployeeDatabaseSetupExtension.class, EmployeeDaoParameterResolver.class })
@ExtendWith(LoggingExtension.class)
@ExtendWith(IgnoreFileNotFoundExceptionExtension.class)
public class EmployeesTest {
private EmployeeJdbcDao employeeDao;
private Logger logger;
public EmployeesTest(EmployeeJdbcDao employeeDao) {
this.employeeDao = employeeDao;
}
@Test
public void whenAddEmployee_thenGetEmployee() throws SQLException {
Employee emp = new Employee(1, "john");
employeeDao.add(emp);
assertEquals(1, employeeDao.findAll().size());
}
@Test
public void whenGetEmployees_thenEmptyList() throws SQLException {
assertEquals(0, employeeDao.findAll().size());
}
public void setLogger(Logger logger) {
this.logger = logger;
}
}
We can see our test class has a constructor with an EmployeeJdbcDao parameter which will be resolved by extending the EmployeeDaoParameterResolver extension.
By adding the EnvironmentExtension, our test will only be executed in an environment different than βqaβ.
Our test will also have the employees table created and each method wrapped in a transaction by adding the EmployeeDatabaseSetupExtension. Even if the whenAddEmployee_thenGetEmploee() test is executed first, which adds one record to the table, the second test will find 0 records in the table.
A logger instance will be added to our class by using the LoggingExtension.
Finally, our test class will ignore all FileNotFoundException instances, since it is adding the corresponding extension.
5.1. Automatic Extension Registration
If we want to register an extension for all tests in our application, we can do so by adding the fully qualified name to the /META-INF/services/org.junit.jupiter.api.extension.Extension file:
com.baeldung.extensions.LoggingExtension
For this mechanism to be enabled, we also need to set the junit.jupiter.extensions.autodetection.enabled configuration key to true. This can be done by starting the JVM with the βDjunit.jupiter.extensions.autodetection.enabled=true property, or by adding a configuration parameter to LauncherDiscoveryRequest:
LauncherDiscoveryRequest request
= LauncherDiscoveryRequestBuilder.request()
.selectors(selectClass("com.baeldung.EmployeesTest"))
.configurationParameter("junit.jupiter.extensions.autodetection.enabled", "true")
.build();
5.2. Programmatic Extension Registration
Although registering extensions using annotations is a more declarative and unobtrusive approach, it has a significant disadvantage: we canβt easily customize the extension behavior. For example, with the current extension registration model, we canβt accept the database connection properties from the client.
In addition to the declarative annotation-based approach, JUnit provides an API to register extensions programmatically. For example, we can retrofit the JdbcConnectionUtil class to accept the connection properties:
public class JdbcConnectionUtil {
private static Connection con;
// no-arg getConnection
public static Connection getConnection(String url, String driver, String username, String password) {
if (con == null) {
// create connection
return con;
}
return con;
}
}
Also, we should add a new constructor for the EmployeeDatabaseSetupExtension extension to support customized database properties:
public EmployeeDatabaseSetupExtension(String url, String driver, String username, String password) {
con = JdbcConnectionUtil.getConnection(url, driver, username, password);
employeeDao = new EmployeeJdbcDao(con);
}
Now, to register the employee extension with custom database properties, we should annotate a static field with the @RegisterExtension annotation:
@ExtendWith({EnvironmentExtension.class, EmployeeDaoParameterResolver.class})
public class ProgrammaticEmployeesUnitTest {
private EmployeeJdbcDao employeeDao;
@RegisterExtension
static EmployeeDatabaseSetupExtension DB =
new EmployeeDatabaseSetupExtension("jdbc:h2:mem:AnotherDb;DB_CLOSE_DELAY=-1", "org.h2.Driver", "sa", "");
// same constrcutor and tests as before
}
Here, weβre connecting to an in-memory H2 database to run the tests.
5.3. Registration Ordering
JUnit registers @RegisterExtension static fields after registering extensions that are declaratively defined using the @ExtendsWith annotation. We can also use non-static fields for programmatic registration, but they will be registered after the test method instantiation and post processors.
If we register multiple extensions programmatically, via @RegisterExtension, JUnit will register those extensions in a deterministic order. Although the ordering is deterministic, the algorithm used for the ordering is non-obvious and internal. To enforce a particular registration ordering, we can use the @Order annotation:
public class MultipleExtensionsUnitTest {
@Order(1)
@RegisterExtension
static EmployeeDatabaseSetupExtension SECOND_DB = // omitted
@Order(0)
@RegisterExtension
static EmployeeDatabaseSetupExtension FIRST_DB = // omitted
@RegisterExtension
static EmployeeDatabaseSetupExtension LAST_DB = // omitted
// omitted
}
Here, extensions are ordered based on priority, where a lower value has greater priority than a higher value. Also, extensions with no @Order annotation would have the lowest possible priority.
6. Conclusion
In this tutorial, we have shown how we can make use of the JUnit 5 extension model to create custom test extensions.
