How do I use JUnit extensions with @ExtendWith?

JUnit Jupiter (JUnit 5) provides a powerful extension model that lets you plug custom behavior into the test lifecycle. Instead of the old JUnit 4 approach of using @RunWith (which allowed only one runner) or @Rule, JUnit 5 uses @ExtendWith — and you can apply as many extensions as you want.

Let’s break this down step by step.


What Is an Extension?

An extension is a class that hooks into the JUnit Jupiter test lifecycle to add behavior such as:

  • setting up resources before tests
  • cleaning up after tests
  • injecting parameters into test methods
  • intercepting test execution
  • handling exceptions
  • conditionally disabling tests

Extensions implement one or more interfaces from the org.junit.jupiter.api.extension package.


The @ExtendWith Annotation

The @ExtendWith annotation registers an extension with a test class or method.

import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(MyExtension.class)
class MyTest {
    // ...
}

You can apply it at the class level, the method level, or even on custom annotations.


Example 1: Using a Built-in Extension (Mockito)

One of the most common uses of @ExtendWith is enabling Mockito’s annotation-based mocking:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void findsUserById() {
        when(userRepository.findById(1L))
            .thenReturn(new User(1L, "Alice"));

        User user = userService.findUser(1L);

        assertNotNull(user);
    }
}

The MockitoExtension handles @Mock and @InjectMocks for you automatically.


Example 2: Applying Multiple Extensions

You can register multiple extensions in a single annotation:

import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith({SpringExtension.class, MockitoExtension.class})
class OrderServiceTest {
    // ...
}

Or, stack them as separate annotations (JUnit 5 supports @ExtendWith as a repeatable annotation):

@ExtendWith(SpringExtension.class)
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    // ...
}

Example 3: Writing Your Own Extension

Let’s write a simple extension that logs how long each test takes.

Step 1 — Create the Extension

import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;

public class TimingExtension
        implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    private static final Namespace NAMESPACE = Namespace.create(TimingExtension.class);
    private static final String START_TIME = "startTime";

    @Override
    public void beforeTestExecution(ExtensionContext context) {
        getStore(context).put(START_TIME, System.currentTimeMillis());
    }

    @Override
    public void afterTestExecution(ExtensionContext context) {
        long startTime = getStore(context).remove(START_TIME, long.class);
        long duration = System.currentTimeMillis() - startTime;

        System.out.printf("Test '%s' took %d ms%n",
                context.getRequiredTestMethod().getName(), duration);
    }

    private Store getStore(ExtensionContext context) {
        return context.getStore(NAMESPACE);
    }
}

Step 2 — Use the Extension

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(TimingExtension.class)
class SlowServiceTest {

    @Test
    void slowOperation() throws InterruptedException {
        Thread.sleep(100);
    }
}

When you run this test, you’ll see something like:

Test 'slowOperation' took 103 ms

Extension Points You Can Implement

Extensions implement one or more of these interfaces:

Interface Purpose
BeforeAllCallback Runs once before all tests in a class
AfterAllCallback Runs once after all tests in a class
BeforeEachCallback Runs before each test method
AfterEachCallback Runs after each test method
BeforeTestExecutionCallback Runs immediately before test execution
AfterTestExecutionCallback Runs immediately after test execution
ParameterResolver Injects parameters into test methods
TestExecutionExceptionHandler Handles exceptions thrown during tests
ExecutionCondition Conditionally enables/disables tests
TestInstancePostProcessor Post-processes test instances after creation

Example 4: Parameter Injection with ParameterResolver

A common pattern is injecting objects directly into test methods:

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;

public class RandomIntExtension implements ParameterResolver {

    @Override
    public boolean supportsParameter(ParameterContext parameterContext,
                                     ExtensionContext extensionContext) {
        return parameterContext.getParameter().getType() == Integer.class;
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext,
                                   ExtensionContext extensionContext) {
        return (int) (Math.random() * 100);
    }
}

Usage:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(RandomIntExtension.class)
class RandomTest {

    @Test
    void receivesRandomInt(Integer randomValue) {
        System.out.println("Random value: " + randomValue);
    }
}

JUnit will call resolveParameter automatically and pass the result into the test.


Example 5: Applying @ExtendWith at the Method Level

You don’t always need to apply extensions to the whole class:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

class MixedTest {

    @Test
    void plainTest() {
        // no extensions
    }

    @Test
    @ExtendWith(TimingExtension.class)
    void timedTest() {
        // only this test uses the extension
    }
}

Example 6: Composing Custom Meta-Annotations

You can bundle @ExtendWith inside your own annotation to keep test classes clean:

import org.junit.jupiter.api.extension.ExtendWith;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith({TimingExtension.class, MockitoExtension.class})
public @interface IntegrationTest {
}

Then in your tests:

@IntegrationTest
class OrderServiceTest {
    // both TimingExtension and MockitoExtension are active
}

This is a great way to standardize test setup across a codebase.


Alternative: Programmatic Registration with @RegisterExtension

If your extension needs configuration at runtime, use @RegisterExtension on a field instead of @ExtendWith:

import org.junit.jupiter.api.extension.RegisterExtension;

class ConfigurableTest {

    @RegisterExtension
    static final MyConfigurableExtension extension =
            MyConfigurableExtension.builder()
                    .withTimeout(500)
                    .build();
}

Use @ExtendWith when the extension has no state to configure. Use @RegisterExtension when you need to configure the extension instance.


Summary

Concept Description
@ExtendWith(SomeExtension.class) Registers an extension declaratively
Applied to a class Extension is active for all tests in that class
Applied to a method Extension is active only for that test method
Multiple extensions Use @ExtendWith({A.class, B.class}) or stack them
Custom meta-annotations Bundle multiple extensions under one annotation
@RegisterExtension Alternative for stateful/configurable extensions

Rule of thumb:

Use @ExtendWith when you want to plug in behavior declaratively.
Use @RegisterExtension when the extension needs runtime configuration.

Extensions are the recommended replacement for JUnit 4’s @RunWith and @Rule — and unlike @RunWith, you can compose as many of them as you need.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.