JUnit 5 isn’t just a testing framework — it’s a platform designed to be extended. The real power of JUnit 5 lies in its Extension Model, which allows developers to hook into the test lifecycle, integrate third-party tools, and create reusable, custom testing utilities. From mocking with Mockito to managing application contexts with Spring, extensions are everywhere.
In this tutorial, we’ll explore the JUnit 5 Extension API, see how popular libraries like Mockito and Testcontainers use it, and learn how to build custom extensions for your projects.
Why Extensions Matter
- Reusability: Encapsulate repetitive setup/teardown logic.
- Integration: Seamlessly connect with frameworks like Spring Boot, Testcontainers, and Mockito.
- Maintainability: Keep test classes clean by externalizing cross-cutting concerns.
- CI/CD Reliability: Automate environment setup for reproducible builds.
Think of extensions as plugins for JUnit 5 — they let you expand the framework without modifying its core.
Built-in Extension Points in JUnit 5
JUnit 5 provides multiple extension interfaces you can implement:
BeforeAllCallback
– Code before all tests in a class.BeforeEachCallback
– Code before each test method.AfterEachCallback
– Code after each test method.AfterAllCallback
– Code after all tests in a class.TestInstancePostProcessor
– Customize test instance creation.ParameterResolver
– Inject parameters into test methods.ExecutionCondition
– Enable/disable tests dynamically.
Example: Using @ExtendWith with Mockito
Mockito integrates with JUnit 5 via MockitoExtension
.
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.*;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository repository;
@InjectMocks
private OrderService service;
@Test
void shouldSaveOrder() {
Order order = new Order("Book");
service.placeOrder(order);
verify(repository).save(order);
}
}
Here, the MockitoExtension
manages mock initialization automatically.
Example: Using SpringExtension
Spring Boot integrates with JUnit 5 via SpringExtension
.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest
class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@Test
void shouldLoadContextAndCreateUser() {
User user = userService.createUser("Alice");
assert user != null;
}
}
The extension ensures the Spring context is loaded before running tests.
Example: Testcontainers with Extensions
Testcontainers provides JUnit 5 integration with reusable container lifecycle management.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
class DatabaseIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
@Test
void testDatabaseIsRunning() {
System.out.println("Postgres running on: " + postgres.getJdbcUrl());
assert postgres.isRunning();
}
}
Sample log output:
Creating container for image: postgres:15
Container started in 12.4s
JDBC URL: jdbc:postgresql://localhost:32789/test
Writing a Custom Extension
Custom extensions are useful for encapsulating repetitive logic.
Example: Timing Extension
import org.junit.jupiter.api.extension.*;
public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create("timing");
@Override
public void beforeTestExecution(ExtensionContext context) {
context.getStore(NAMESPACE).put("start", System.currentTimeMillis());
}
@Override
public void afterTestExecution(ExtensionContext context) {
long start = context.getStore(NAMESPACE).remove("start", long.class);
long duration = System.currentTimeMillis() - start;
System.out.println(context.getDisplayName() + " took " + duration + "ms");
}
}
Applying Custom Extension
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(TimingExtension.class)
class PerformanceTest {
@Test
void slowTest() throws InterruptedException {
Thread.sleep(200);
}
}
Output:
slowTest() took 200ms
Real-World Scenarios
- Spring Boot Apps: Use
SpringExtension
for dependency injection in tests. - Microservices: Combine Testcontainers with extensions to manage Kafka, Redis, or PostgreSQL.
- Legacy Codebases: Add timing/logging extensions to measure slow test methods.
- CI/CD Pipelines: Automate conditional test execution based on environment variables.
Best Practices
- Keep extensions focused on cross-cutting concerns (timing, logging, setup).
- Prefer reusable extensions across projects.
- Combine built-in and third-party extensions strategically.
- Document extension usage for contributors.
- Use parameter resolvers to simplify test method signatures.
Version Tracker
- JUnit 4 → JUnit 5: The
@Rule
and@ClassRule
APIs were replaced with the extension model. - Mockito Updates:
MockitoExtension
improved static and final mocking support. - Testcontainers Growth: New modules and reusable JUnit 5 integrations simplify container orchestration.
Conclusion & Key Takeaways
JUnit 5’s extension model transforms testing into a flexible, pluggable ecosystem. Whether you’re using third-party extensions like Mockito and Spring, or writing your own, extensions help streamline testing, improve maintainability, and enhance CI/CD workflows.
Key Takeaways:
- Extensions hook into the JUnit 5 lifecycle for setup, teardown, and customization.
- Use
@ExtendWith
to apply built-in, third-party, or custom extensions. - Popular extensions include Mockito, Spring, and Testcontainers.
- Custom extensions add cross-cutting functionality like timing and logging.
FAQ
1. What replaced @Rule in JUnit 5?
JUnit 5 extensions replaced @Rule
and @ClassRule
from JUnit 4.
2. How do I apply multiple extensions?
You can use multiple @ExtendWith
annotations or a composite annotation.
3. Can I register extensions programmatically?
Yes, via @RegisterExtension
for more control.
4. Do extensions work with parameterized tests?
Yes, they integrate seamlessly.
5. What is a ParameterResolver?
An extension that injects dependencies directly into test methods.
6. Can I disable tests conditionally with extensions?
Yes, implement ExecutionCondition
to enable/disable tests dynamically.
7. Do IDEs support custom extensions?
Yes, they work like standard JUnit 5 tests.
8. Can I use multiple third-party extensions in one test class?
Yes, JUnit 5 supports stacking multiple extensions.
9. Should I migrate from JUnit 4 rules to JUnit 5 extensions?
Yes, extensions are more powerful and flexible.
10. Are extensions useful in CI/CD pipelines?
Absolutely, they automate setup/teardown and make pipelines reproducible.