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. If 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 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 javax.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 javax.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); } }