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.0.0-M5</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.extensions.autodetection.enabled configuration key to true. This can be done by starting the JVM with the –Djunit.extensions.autodetection.enabled=true property, or by adding a configuration parameter to LauncherDiscoveryRequest:
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() .selectors(selectClass("com.baeldung.EmployeesTest")) .configurationParameter("junit.extensions.autodetection.enabled", "true") .build();
6. Conclusion
In this tutorial, we have shown how we can make use of the JUnit 5 extension model to create custom test extensions.
The full source code of the examples can be found over on GitHub.