Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 4536

A Guide to JUnit 5 Extensions

$
0
0

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:

  1. BeforeAllCallback
  2. BeforeAll
  3. BeforeEachCallback
  4. BeforeEach
  5. BeforeTestExecutionCallback
  6. Test
  7. AfterTestExecutionCallback
  8. AfterEach
  9. AfterEachCallback
  10. AfterAll
  11. 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.


Viewing all articles
Browse latest Browse all 4536

Trending Articles