Writing clean, maintainable tests often requires injecting dependencies instead of manually creating them. In production code, frameworks like Spring handle dependency injection seamlessly. But did you know that JUnit 5 also supports dependency injection out of the box? Combined with extensions like Mockito and Spring, DI in tests ensures cleaner code, reduces boilerplate, and makes test suites more flexible.
In this tutorial, we’ll explore dependency injection in JUnit 5 tests, starting with built-in injection features, moving to Mockito and Spring integration, and ending with real-world scenarios using Testcontainers and CI/CD pipelines.
Why Dependency Injection Matters in Testing
- Maintainability: Cleaner and less repetitive test code.
- Flexibility: Swap implementations (e.g., real vs mock) with ease.
- Isolation: Inject test doubles for unit tests.
- Reproducibility: Inject configurations for consistent CI/CD builds.
- Scalability: Enables structured and modular test suites.
Think of DI in tests like a stage play — instead of each actor (test) building their own props, the props are provided to them so they can focus on performance.
JUnit 5 Built-in Dependency Injection
JUnit 5 provides injection through ParameterResolver
. Several objects can be injected automatically:
TestInfo
– Metadata about the current test.TestReporter
– Publish test execution reports.RepetitionInfo
– For repeated tests.TempDir
– Temporary directories for file testing.
Example: Injecting TestInfo and TestReporter
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestReporter;
class BuiltInInjectionTest {
@Test
void testWithInjectedParams(TestInfo testInfo, TestReporter reporter) {
reporter.publishEntry("Running test: " + testInfo.getDisplayName());
}
}
Output:
Running test: testWithInjectedParams(TestInfo, TestReporter)
Dependency Injection with Mockito
Mockito integrates with JUnit 5 using the MockitoExtension
, injecting mocks automatically.
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.verify;
@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, Mockito handles DI by injecting the repository
into OrderService
.
Dependency Injection with Spring
For integration tests, Spring Boot provides DI via the 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 shouldCreateUserWithSpringDI() {
User user = userService.createUser("Alice");
assert user != null;
}
}
Spring injects the UserService
bean directly, ensuring real context testing.
Dependency Injection with Testcontainers
Testcontainers also integrates with JUnit 5 via DI for container lifecycle.
import org.junit.jupiter.api.Test;
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 shouldStartContainer() {
System.out.println("Postgres running on: " + postgres.getJdbcUrl());
assert postgres.isRunning();
}
}
Testcontainers automatically injects lifecycle management of the container.
Sample log:
Creating container for image: postgres:15
Container started in 12.1s
JDBC URL: jdbc:postgresql://localhost:32789/test
Writing a Custom ParameterResolver
JUnit 5 lets you create custom DI providers with ParameterResolver
.
import org.junit.jupiter.api.extension.*;
public class RandomStringParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType() == String.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return "InjectedString-" + System.currentTimeMillis();
}
}
Applying Custom Resolver
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(RandomStringParameterResolver.class)
class CustomInjectionTest {
@Test
void testWithInjectedString(String injected) {
System.out.println("Received: " + injected);
}
}
Output:
Received: InjectedString-1702469912345
Real-World Scenarios
- Spring Boot Apps: Autowire repositories and services in integration tests.
- Microservices: Inject API clients or Testcontainers for real-world scenarios.
- Legacy Codebases: Replace static factories with injected mocks using Mockito.
- CI/CD Pipelines: Inject environment-specific configurations dynamically.
Best Practices
- Use DI to reduce boilerplate setup code.
- Combine Mockito for unit tests and Spring/Testcontainers for integration tests.
- Keep unit tests fast by mocking heavy dependencies.
- Leverage
ParameterResolver
for reusable custom injections. - Document your DI strategy for contributors.
Version Tracker
- JUnit 4 → JUnit 5: Moved from manual
@Before
setup to extensible DI withParameterResolver
. - Mockito Updates: Added static/final mocking, enhancing injection scenarios.
- Testcontainers Growth: Ecosystem expanded with modules for Kafka, Redis, LocalStack, etc.
Conclusion & Key Takeaways
Dependency injection in JUnit 5 tests makes test suites cleaner, maintainable, and scalable. Whether through built-in injection, Mockito mocks, Spring beans, or Testcontainers, DI ensures that tests remain flexible and production-ready.
Key Takeaways:
- JUnit 5 supports DI with
ParameterResolver
for custom injections. - Mockito provides automatic mock injection with
@InjectMocks
. - Spring Boot offers powerful integration via
@SpringBootTest
. - Testcontainers integrates lifecycle management seamlessly.
FAQ
1. What is the role of ParameterResolver in JUnit 5?
It allows custom injection of parameters into test methods.
2. How is DI different in unit vs integration tests?
Unit tests use mocks (Mockito), while integration tests use real beans/containers (Spring, Testcontainers).
3. Can I use multiple extensions together?
Yes, you can stack @ExtendWith
annotations or use @RegisterExtension
.
4. How do I mock static methods in JUnit 5?
Use Mockito’s mockStatic
feature introduced in recent versions.
5. Do I need Spring to use DI in tests?
No, JUnit 5 provides its own injection via ParameterResolver
.
6. Can I inject dependencies into constructors?
Yes, JUnit 5 supports constructor injection for test classes.
7. How do I inject environment variables in CI/CD?
Use System.getenv()
with assumptions or custom resolvers.
8. Do DI features work with parameterized tests?
Yes, DI integrates seamlessly with parameterized tests.
9. Can Testcontainers inject databases automatically?
Yes, containers are managed and injected via annotations like @Container
.
10. Should I migrate from JUnit 4 to JUnit 5 for DI?
Yes, JUnit 5’s DI features are far more flexible and powerful.