The JUnit 6 Extensions Model is one of the most powerful features in the framework. Instead of subclassing a test runner or using test rules (the old JUnit 4 approach), JUnit 6 lets you inject behaviour at precise points in the test lifecycle through clean, composable extension interfaces. This guide walks you through every major extension point with real, complete examples you can drop into your project today.
If you want to understand the architecture behind extensions, see JUnit 6 Architecture Deep Dive first. This guide focuses on practical implementation.
What Is a JUnit 6 Extension?
An extension is a class that implements one or more of JUnit 6βs callback interfaces. JUnit calls these callbacks at specific points in the test lifecycle β before the test runs, after it runs, when parameters need to be resolved, when exceptions need to be handled, and more.
Extensions replace everything that required @RunWith, @Rule, and @ClassRule in JUnit 4, but in a cleaner, more composable way.
Registering an Extension
There are three ways to register an extension:
// 1. Declarative: @ExtendWith on the test class or method
@ExtendWith(TimingExtension.class)
class OrderServiceTest { ... }
// 2. Programmatic: @RegisterExtension on a field (allows configuration)
class OrderServiceTest {
@RegisterExtension
static DatabaseExtension database = new DatabaseExtension("jdbc:h2:mem:test");
}
// 3. Automatic: via ServiceLoader in META-INF/services
// File: src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
// Contents: com.example.extensions.TimingExtension
Extension 1: Lifecycle Callbacks β Test Timing Logger
import org.junit.jupiter.api.extension.*;
import java.lang.reflect.Method;
/**
* TimingExtension: logs how long each test method takes to execute.
* Implements BeforeTestExecutionCallback and AfterTestExecutionCallback
* to bracket the actual test method execution.
*/
public class TimingExtension
implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
// Key used to store start time in the extension context store
private static final String START_TIME_KEY = "startTime";
@Override
public void beforeTestExecution(ExtensionContext context) {
// Store start time in the extension's namespace store
// The store is scoped to this extension + this test method
getStore(context).put(START_TIME_KEY, System.currentTimeMillis());
}
@Override
public void afterTestExecution(ExtensionContext context) {
// Retrieve the stored start time
long startTime = getStore(context).remove(START_TIME_KEY, long.class);
long durationMs = System.currentTimeMillis() - startTime;
// Get the test method name for the log message
String testMethodName = context.getRequiredTestMethod().getName();
System.out.printf("[TimingExtension] %-60s %4d ms%n",
testMethodName, durationMs);
}
// Helper: creates a namespace-scoped store for this extension
private ExtensionContext.Store getStore(ExtensionContext context) {
return context.getStore(
ExtensionContext.Namespace.create(getClass(), context.getRequiredTestMethod())
);
}
}
// Usage
@ExtendWith(TimingExtension.class)
class PaymentServiceTest {
@Test
void processingPaymentCompletesSuccessfully() throws Exception {
Thread.sleep(42); // simulates work
assertTrue(true);
}
}
[TimingExtension] processingPaymentCompletesSuccessfully 42 ms
Tests run: 1, Failures: 0
Continue reading JUnit 6 Extensions Model: Build Custom Extensions Step-by-Step