In the evolution of Java testing, the transition from JUnit 4 to JUnit 5 (Jupiter) wasn’t just a version bump—it was a complete architectural overhaul. One of the most significant changes was the retirement of the rigid Runner and Rule API in favor of a flexible, modular Extension Model.
At the heart of this model lies the @ExtendWith annotation. In this comprehensive guide, we will explore why @ExtendWith is a gateway into a sophisticated ecosystem that boosts developer productivity, how to use it with industry-standard libraries like Mockito, and how to build your own custom extensions to automate repetitive testing logic.
What is JUnit 5 @ExtendWith?
In JUnit 5, the @ExtendWith annotation is the primary way to register one or more Extensions for a test class or a specific test method.
Think of an extension as a “plugin” for your tests. Instead of JUnit being a closed system, it provides Extension Points—specific moments in the test lifecycle (like before a test starts, after it fails, or when a parameter needs to be resolved). The @ExtendWith annotation tells JUnit: “Hey, use this specific class to intercept these lifecycle events for me.”
Key Advantages: Composition over Inheritance
- True Composability: In JUnit 4, you were limited to a single
@RunWithannotation. JUnit 5 allows you to stack as many@ExtendWithdeclarations as you need. - Granular Scope: You can apply extensions to an entire class, a nested inner class, or even a single
@Testmethod. - Unified API: It eliminates the confusing distinction between
Runners,MethodRules, andTestRules.
JUnit 5 Extension Lifecycle at a Glance
Understanding when your code executes is critical for debugging. Here is the conceptual flow of the Lifecycle Callbacks:
[ Test Discovery ]
│
▼
[ BeforeAllCallback ]
(Once per class)
│
▼
[ BeforeEachCallback ]
(Before every @Test)
│
▼
[ BeforeTestExecutionCallback ]
(Immediately before test method)
│
▼
( @Test Method )
│
▼
[ AfterTestExecutionCallback ]
(Immediately after test method)
│
▼
[ AfterEachCallback ]
(After every @Test)
│
▼
[ AfterAllCallback ]
(Once per class)
JUnit 4 @RunWith vs. JUnit 5 @ExtendWith
| Feature | JUnit 4 (@RunWith / @Rule) | JUnit 5 (@ExtendWith) |
| Cardinality | Only one Runner per class | Multiple extensions allowed |
| Integration | Difficult to combine frameworks | Seamlessly combine Mockito, Spring, etc. |
| Logic Reuse | Done via Rules and Base Classes | Done via reusable Extension classes |
| Example | MockitoJUnitRunner.class | MockitoExtension.class |
Integrating Mockito with @ExtendWith
The MockitoExtension automatically initializes mocks and manages the mock session lifecycle.
Step 1: Add the Dependency (Maven)
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.23.0</version>
<scope>test</scope>
</dependency>
Step 2: Implement the Test
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.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private PaymentGateway paymentGateway;
@InjectMocks
private OrderService orderService;
@Test
void shouldProcessPaymentSuccessfully() {
when(paymentGateway.authorize(100.0)).thenReturn(true);
boolean result = orderService.processOrder(100.0);
assertTrue(result, "Payment should be authorized");
}
}
Console Output:
[INFO] Scanning for projects...
[INFO] Running OrderServiceTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.852 s - in OrderServiceTest
Power Through Composition: Multiple Extensions
Stacking extensions allows you to combine heavy lifting like Spring with mock management.
@ExtendWith(SpringExtension.class)
@ExtendWith(MockitoExtension.class)
@SpringBootTest
class UserProfileServiceTest {
@Mock
private ExternalService externalService;
@Test
void testWithSpringAndMockito() {
// Test logic
}
}
Console Output:
2024-05-20 10:00:01.452 INFO ... Starting UserProfileServiceTest
2024-05-20 10:00:03.120 INFO ... Started UserProfileServiceTest in 1.668 seconds
[INFO] Running UserProfileServiceTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Building Your Own Custom Extension
Example 1: Performance Tracker
Using the ExtensionContext.Store ensures your extension is thread-safe and scoped correctly.
public class PerformanceTrackerExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final String START_TIME = "START_TIME";
@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(">>> PERFORMANCE: Test [%s] took %d ms%n", context.getDisplayName(), duration);
}
private ExtensionContext.Store getStore(ExtensionContext context) {
return context.getStore(ExtensionContext.Namespace.create(getClass(), context.getRequiredTestMethod()));
}
}
Console Output (when applied to a test):
>>> PERFORMANCE: Test [myTest()] took 42 ms
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Example 2: Database Transaction & Clean-up
In enterprise apps, you often want to wrap tests in a transaction that rolls back automatically to keep the DB clean.
public class TransactionalExtension implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
System.out.println(">>> DB: Starting Transaction for " + context.getDisplayName());
// Logic to begin DB transaction
}
@Override
public void afterEach(ExtensionContext context) {
System.out.println(">>> DB: Rolling back Transaction for " + context.getDisplayName());
// Logic to rollback DB transaction
}
}
Usage & Output:
@ExtendWith(TransactionalExtension.class)
class UserRepositoryTest {
@Test
void testUserSave() {
System.out.println("Executing: Saving User to DB");
}
}
Console Output:
>>> DB: Starting Transaction for testUserSave()
Executing: Saving User to DB
>>> DB: Rolling back Transaction for testUserSave()
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Advanced: Programmatic Registration with @RegisterExtension
@RegisterExtension is used when you need to pass dynamic data to an extension that can’t be handled via class-level annotations.
class FileSystemTest {
@RegisterExtension
static TempDirectoryExtension tempDir = new TempDirectoryExtension("/custom/path/logs");
@Test
void testLogCreation() {
System.out.println("Using directory: " + tempDir.getPath());
}
}
Console Output:
>>> EXTENSION: Initializing directory at /custom/path/logs
Using directory: /custom/path/logs
>>> EXTENSION: Cleaning up directory at /custom/path/logs
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Conclusion: Your Learning Journey
- Start Simple: Use
MockitoExtensionto eliminate setup boilerplate. - Combine Forces: Use multiple
@ExtendWithdeclarations for integration tests. - Go Programmatic: Use
@RegisterExtensionfor dynamic configurations. - Innovate: Build custom extensions for shared concerns like Security Context Injection, Database Clean-up, or API Logging.