JUnit 5's extension model is one of its biggest improvements over JUnit 4. Extensions let you hook into the test lifecycle, add custom behavior, and share logic across test classes. Think of them as plugins for your test framework.
Extension Points
JUnit 5 provides these extension points:
+--------------------------------+----------------------------------------+
| Interface | Purpose |
+--------------------------------+----------------------------------------+
| BeforeAllCallback | Runs before all tests in a class |
| AfterAllCallback | Runs after all tests in a class |
| BeforeEachCallback | Runs before each test method |
| AfterEachCallback | Runs after each test method |
| BeforeTestExecutionCallback | Runs right before test execution |
| AfterTestExecutionCallback | Runs right after test execution |
| TestInstancePostProcessor | Process test instance after creation |
| TestWatcher | Watches test execution |
| ParameterResolver | Resolves test method parameters |
| ExceptionHandler | Handles exceptions during tests |
+--------------------------------+----------------------------------------+
Your First Extension
import org.junit.jupiter.api.extension.*;
import java.lang.annotation.*;
public class TimingExtension implements BeforeTestExecutionCallback,
AfterTestExecutionCallback {
private static final String START_TIME = "start time";
@Override
public void beforeTestExecution(ExtensionContext context) {
context.getStore(ExtensionContext.Namespace.GLOBAL)
.put(START_TIME, System.nanoTime());
}
@Override
public void afterTestExecution(ExtensionContext context) {
long startTime = context.getStore(ExtensionContext.Namespace.GLOBAL)
.get(START_TIME, Long.class);
long duration = System.nanoTime() - startTime;
String testName = context.getDisplayName();
System.out.printf("Test %s took %.2f ms%n",
testName, duration / 1_000_000.0);
}
}
// Register the extension
@ExtendWith(TimingExtension.class)
class MyTest {
@Test
void shouldRunQuickly() {
// After this test, TimingExtension prints duration
}
}
Annotation-Driven Extensions
The most powerful pattern is creating a custom annotation that registers an extension:
// Step 1: Create the annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(TimingExtension.class)
public @interface Timed {
}
// Step 2: Use it
class MyTest {
@Test
@Timed
void someTest() {
// This test is automatically timed
}
}
ParameterResolver Extension
public class RandomAnnotation
implements ParameterResolver {
@Override
public boolean supportsParameter(
ParameterContext context,
ExtensionContext extensionContext) {
return context.isAnnotated(Random.class);
}
@Override
public Object resolveParameter(
ParameterContext context,
ExtensionContext extensionContext) {
Random random = context.findAnnotation(Random.class).get();
return (int) (Math.random() * random.max()) + 1;
}
}
// Usage
@Test
void randomTest(@Random(max = 100) int number) {
assertTrue(number > 0 && number <= 100);
}
TemporaryDirectory Extension
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
class FileTest {
@TempDir
Path tempDir;
@Test
void shouldWriteToFile() throws IOException {
Path file = tempDir.resolve("test.txt");
Files.writeString(file, "hello");
assertEquals("hello", Files.readString(file));
}
// JUnit automatically cleans up the directory
}
Conditional Extensions
public class LinuxOnlyCondition implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluateExecutionCondition(
ExtensionContext context) {
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("linux")) {
return ConditionEvaluationResult.enabled("Running on Linux");
}
return ConditionEvaluationResult.disabled("Not on Linux, skipping");
}
}
// Create an annotation for it
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(LinuxOnlyCondition.class)
public @interface LinuxOnly {
}
// Usage
@LinuxOnly
class LinuxSpecificTest {
@Test
void linuxTest() {
// Only runs on Linux
}
}
Registered Extensions (Global)
Instead of annotating every test class, register extensions globally:
// META-INF/services/org.junit.jupiter.api.extension.Extension
com.example.TimingExtension
com.example.LoggingExtension
Or in junit-platform.properties:
junit.jupiter.extensions.enabled=true
junit.jupiter.extensions.autodetection.enabled=true
Extension Store
public class DatabaseExtension implements
BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
Connection conn = createConnection();
getStore(context).put("connection", conn);
}
@Override
public void afterEach(ExtensionContext context) {
Connection conn = getStore(context)
.get("connection", Connection.class);
conn.close();
}
private Store getStore(ExtensionContext context) {
return context.getStore(
ExtensionContext.Namespace.create(DatabaseExtension.class)
);
}
}