JUnit 5 Extensions: Extending the Testing Framework

Illustration for JUnit 5 Extensions: Extending the Testing Framework
By Last updated:

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

  1. Spring Boot Apps: Use SpringExtension for dependency injection in tests.
  2. Microservices: Combine Testcontainers with extensions to manage Kafka, Redis, or PostgreSQL.
  3. Legacy Codebases: Add timing/logging extensions to measure slow test methods.
  4. 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.