Modern Java applications require robust testing strategies to ensure stability, scalability, and reliability. Two of the most widely used frameworks in the Java ecosystem are JUnit 5 and Mockito. While JUnit 5 provides the foundation for writing and executing tests, Mockito simplifies mocking dependencies, allowing developers to test classes in isolation.
In this tutorial, we’ll explore how to combine Mockito with JUnit 5, covering setup, usage, best practices, and advanced scenarios. Whether you’re working on Spring Boot microservices or a simple library, this combination enables clean, maintainable, and production-ready test suites.
Why Combine JUnit 5 and Mockito?
JUnit 5 and Mockito complement each other:
- JUnit 5: Offers modern annotations (
@Test
,@BeforeEach
,@AfterEach
), parameterized tests, extensions, and better IDE/tooling integration. - Mockito: Provides a flexible mocking framework to replace dependencies with mocks, spies, or stubs.
Together, they allow developers to:
- Isolate business logic from external systems (databases, APIs, file systems).
- Prevent flaky or slow tests by mocking slow dependencies.
- Write TDD/BDD-style test cases that are both expressive and maintainable.
- Integrate seamlessly into CI/CD pipelines with tools like Jenkins and GitHub Actions.
Project Setup
Maven Dependency
<dependencies>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- Mockito Core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- Mockito JUnit 5 Integration -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Gradle Dependency
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
testImplementation 'org.mockito:mockito-core:5.10.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.10.0'
Writing Your First Test with Mockito and JUnit 5
Suppose we have a UserService
that depends on a UserRepository
. We want to test the business logic of UserService
without hitting the database.
Example Classes
public class User {
private String id;
private String name;
// constructors, getters, setters
}
public interface UserRepository {
User findById(String id);
}
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String getUserName(String id) {
User user = userRepository.findById(id);
return user != null ? user.getName() : "Unknown";
}
}
Test with JUnit 5 + Mockito
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.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void testGetUserName_UserExists() {
User mockUser = new User("1", "Alice");
when(userRepository.findById("1")).thenReturn(mockUser);
String result = userService.getUserName("1");
assertEquals("Alice", result);
verify(userRepository).findById("1");
}
@Test
void testGetUserName_UserNotFound() {
when(userRepository.findById("2")).thenReturn(null);
String result = userService.getUserName("2");
assertEquals("Unknown", result);
}
}
Here’s what happens:
@ExtendWith(MockitoExtension.class)
integrates Mockito with JUnit 5.@Mock
creates a mock instance ofUserRepository
.@InjectMocks
automatically injects the mock intoUserService
.when(...).thenReturn(...)
stubs a method call.verify(...)
ensures that the method was actually invoked.
Advanced Usage
Parameterized Tests with Mocks
@ParameterizedTest
@ValueSource(strings = {"1", "2"})
void testGetUserNameWithMultipleIds(String userId) {
when(userRepository.findById(userId)).thenReturn(new User(userId, "User" + userId));
String result = userService.getUserName(userId);
assertTrue(result.startsWith("User"));
}
Combining with Testcontainers
When integration tests require databases or external services, use Testcontainers. For example:
@Testcontainers
class UserRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
@Test
void testDatabaseConnection() {
assertTrue(postgres.isRunning());
}
}
This way, you can mix unit tests (Mockito) with integration tests (Testcontainers) under JUnit 5.
Best Practices
- Mock only external dependencies – Don’t mock the class under test.
- Verify behavior, not implementation – Check observable outcomes.
- Use
@InjectMocks
sparingly – Prefer explicit constructor injection for readability. - Avoid overspecifying interactions – Verify only what matters.
- Combine with CI/CD – Run both unit and integration tests in pipelines.
Version Tracker
- JUnit 4 → JUnit 5: Introduced
@ExtendWith
and modern APIs. - Mockito Updates: Added support for mocking
final
andstatic
methods. - Testcontainers: Expanded ecosystem with modules for DBs, Kafka, LocalStack, and Kubernetes integration.
Conclusion
JUnit 5 and Mockito together provide a powerful, flexible, and production-ready testing stack. By combining mocking with modern test execution, developers can confidently build applications that are well-tested, maintainable, and ready for continuous delivery.
Key Takeaways
- Use JUnit 5 + Mockito for clean and maintainable unit tests.
- Mock dependencies, not the class under test.
- Leverage advanced features like parameterized tests, Testcontainers, and CI/CD integration.
- Follow best practices to keep your tests reliable and scalable.
FAQ
1. What’s the difference between unit and integration tests?
Unit tests isolate logic with mocks, while integration tests check real interactions with databases or services.
2. How do I mock a static method in Mockito?
Use mockStatic()
(Mockito 3.4+) or PowerMock for legacy cases.
3. How can Testcontainers help in CI/CD pipelines?
They spin up disposable Docker containers, ensuring consistent environments in local and CI builds.
4. What is the difference between TDD and BDD?
TDD focuses on test-first development, while BDD emphasizes behavior and collaboration using human-readable tests.
5. How do I fix flaky tests in Java?
Avoid randomness, mock external systems, and use retries or timeouts where appropriate.
6. Can I use Mockito with Spring Boot?
Yes, with @MockBean
and @SpringBootTest
annotations for integration.
7. Is it good to mix mocks and real services?
Use mocks for unit tests and real services (via Testcontainers) for integration.
8. What’s the role of verify()
in Mockito?
It ensures expected interactions happened, improving test correctness.
9. Should I mock private methods?
No, redesign the class to test behavior via public APIs instead.
10. How do I measure code coverage?
Use JaCoCo with Maven/Gradle to generate coverage reports integrated with JUnit tests.