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
@ExtendWithwhen you want to plug in behavior declaratively.
Use@RegisterExtensionwhen 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.
