Spring Boot Mastery: Mockito Tutorial | @Mock, @InjectMocks, and @MockBean
In modern Java development, unit testing is essential for validating business logic in isolation. However, relying on the full Spring ApplicationContext for every test slows down execution significantly due to component scanning, bean instantiation, and external database connections. This chapter covers Mockito integration in Spring Boot, detailing how to isolate service logic using @Mock and @InjectMocks for fast execution, while explaining when to use Spring's @MockBean.
1. The Problem: Spring Context Overhead in Unit Tests
When testing a Spring Boot component (such as a service layer class), the standard approach often begins with integration testing using @SpringBootTest and @Autowired.
@SpringBootTest
class UserDetailsServiceImplTest {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Test
void testLoadUserByUsername() {
UserDetails user = userDetailsService.loadUserByUsername("ram");
assertNotNull(user);
}
}
Why This Slows Down Development
- ApplicationContext Initialization: Spring must scan the entire classpath, create beans, and establish dependency injection graphs.
- External Resource Connections: Databases (e.g., MongoDB, PostgreSQL) and cache servers (e.g., Redis) are initialized during context startup.
- Execution Delay: Even a small application can take 5+ seconds to boot up for a single test. In enterprise applications with hundreds of tests, execution times can stretch into minutes or hours.
Real-Life Analogy
The Restaurant Analogy: Imagine you want to test a single sauce recipe created by a chef. Starting the full Spring ApplicationContext is like building the entire restaurant, hiring waiting staff, and turning on all kitchen appliances just to taste one spoonful of sauce. Mocking allows you to test the chef's recipe in isolation on a simple prep table without opening the restaurant.
2. Core Mental Model: What is Mocking?
Mocking is the process of replacing a real software dependency with a simulated ("fake") object whose behavior is controlled programmatically during a test run.
In a typical multi-tier architecture:
When we mock UserRepository, we sever the connection to MongoDB:
UserDetailsServiceImpl (Unit Under Test)
|
| calls findByUsername("ram")
v
Mock UserRepository (Returns predefined User object immediately)
By substituting a fake UserRepository, the service layer's conditional logic, transformations, and assertions are verified without touching a real database.
3. Understanding the Annotations: @Mock, @InjectMocks, and @MockBean
To write fast, isolated tests, you must know when and where to use each Mockito and Spring Test annotation.
Annotations Breakdown
@Mock (Pure Mockito)
Creates a bare mock instance of a class or interface without starting any Spring context.
- Package:
org.mockito.Mock - Context: Used in standard unit tests.
@InjectMocks (Pure Mockito)
Creates an instance of the class under test and automatically injects all fields annotated with @Mock into it.
- Package:
org.mockito.InjectMocks - Context: Used in standard unit tests.
@MockBean (Spring Test Framework)
Creates a Mockito mock and registers it directly inside Spring’s ApplicationContext, replacing any existing real bean of the same type.
- Package:
org.springframework.boot.test.mock.mockito.MockBean - Context: Used in integration tests annotated with
@SpringBootTest.
4. Code Evolution: From Naive Integration Test to Pure Unit Test
Let us trace how a test class evolves from a slow integration test to a lightweight Mockito unit test using a real-world example (UserDetailsServiceImpl depending on UserRepository).
Stage 1: The Naive Integration Attempt (Flawed)
Mixing @SpringBootTest with @Mock directly leads to errors:
@SpringBootTest
public class UserDetailsServiceImplTest {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Mock
private UserRepository userRepository; // Fails to inject into Spring's UserDetailsServiceImpl
@Test
void testLoadUserByUsername() {
when(userRepository.findByUsername("ram")).thenReturn(new User("ram", "pass123"));
// Output: Still connects to the real Mongo database!
UserDetails user = userDetailsService.loadUserByUsername("ram");
assertNotNull(user);
}
}
Warning: Plain
@Mockinstances are not recognized by Spring'sApplicationContext. BecauseuserDetailsServicewas@Autowiredfrom the Spring context, Spring injected the realUserRepositorybean into it, ignoring the@Mockfield.
Stage 2: The @MockBean Approach (Working, but Slow)
To make Spring use a mock inside its container, replace @Mock with @MockBean:
@SpringBootTest
public class UserDetailsServiceImplTest {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@MockBean
private UserRepository userRepository; // Replaces the UserRepository bean inside Spring Context
@Test
void testLoadUserByUsername() {
User dummyUser = User.builder()
.username("ram")
.password("encrypted_pass")
.roles(new ArrayList<>())
.build();
when(userRepository.findByUsername("ram")).thenReturn(dummyUser);
UserDetails user = userDetailsService.loadUserByUsername("ram");
assertNotNull(user);
}
}
Why Stage 2 is Suboptimal
While @MockBean successfully overrides the repository with a fake instance, the test still requires @SpringBootTest. Spring must boot up the container, taking seconds for every test execution.
Stage 3: The Pure Mockito Approach (Fast & Decoupled)
Remove all Spring annotations (@SpringBootTest, @Autowired, @MockBean) and use pure Mockito annotations (@InjectMocks, @Mock):
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
public class UserDetailsServiceImplTest {
@InjectMocks
private UserDetailsServiceImpl userDetailsService;
@Mock
private UserRepository userRepository;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this); // Initializes fields annotated with @Mock and @InjectMocks
}
@Test
void testLoadUserByUsername() {
User dummyUser = User.builder()
.username("ram")
.password("encrypted_pass")
.roles(new ArrayList<>())
.build();
// Stubbing repository response
when(userRepository.findByUsername("ram")).thenReturn(dummyUser);
UserDetails user = userDetailsService.loadUserByUsername("ram");
assertNotNull(user);
}
}
5. Mock Initialization & Lifecycle
When avoiding @SpringBootTest, Spring is not managing object creation. If you do not explicitly initialize Mockito, fields annotated with @Mock and @InjectMocks will remain null, causing a NullPointerException.
Step-by-Step Initialization Sequence
Note: Modern JUnit 5 tests can also use
@ExtendWith(MockitoExtension.class)on the test class level as an alternative to manually invokingMockitoAnnotations.openMocks(this)inside@BeforeEach.
6. Stubbing Behavior: when(...).thenReturn(...)
Stubbing instructs a mock object on how to react when a specific method is invoked.
Exact Argument Matching
when(userRepository.findByUsername("ram")).thenReturn(dummyUser);
- Behavior: Returns
dummyUseronly iffindByUsernameis called with the exact string"ram". Passing"shyam"will returnnull.
Flexible Argument Matching
when(userRepository.findByUsername(ArgumentMatchers.anyString())).thenReturn(dummyUser);
- Behavior: Returns
dummyUserregardless of which string value is passed intofindByUsername.
7. When to Use Pure Mockito vs. @MockBean
While pure Mockito is preferred for speed, specific architecture scenarios necessitate @MockBean.
Scenario: Hybrid Integration Testing (e.g., Real Cache + Mock Database)
If a service interacts with both a Redis cache and MongoDB, and you want to test the actual Spring Redis auto-configuration while stubbing out MongoDB calls:
In this scenario:
- Pure Mockito cannot be used because you require real Spring components (e.g.,
RedisTemplate). @MockBeanis the correct choice because it leaves the Spring container intact while selectively replacingUserRepository.
8. Comparative Synthesis
9. Common Pitfalls and Debugging Guide
Pitfall 1: NullPointerException on Service Execution
- Symptom: Calling a method on the class annotated with
@InjectMocksthrowsNullPointerException. - Root Cause: Missing
MockitoAnnotations.openMocks(this)or missing@ExtendWith(MockitoExtension.class). - Fix: Ensure initialization code is present in
@BeforeEach.
Pitfall 2: Tests Still Hitting the Real Database
- Symptom: Logs show database connection attempts (e.g., MongoDB driver logs) during test run.
- Root Cause:
@SpringBootTestis present, and plain@Mockwas used instead of@MockBean. - Fix: Remove
@SpringBootTestand convert to a pure unit test, or change@Mockto@MockBean.
Pitfall 3: Sub-dependencies Returning null Unexpectedly
- Symptom: Calling
user.getRoles()throwsNullPointerExceptioninside the service method. - Root Cause: Dummy object returned by
when().thenReturn()was incompletely built. - Fix: Ensure dummy objects created via Builders or constructors initialize required collection fields (e.g.,
roles = new ArrayList<>()).
10. Complete Code Flow Walkthrough
Below is a complete, self-contained example demonstrating pure unit testing of UserDetailsServiceImpl using Mockito.
1. Service Implementation Under Test
package com.example.service;
import com.example.entity.User;
import com.example.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found: " + username);
}
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}
2. Mockito Unit Test
package com.example.service;
import com.example.entity.User;
import com.example.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
class UserDetailsServiceImplTest {
@InjectMocks
private UserDetailsServiceImpl userDetailsService;
@Mock
private UserRepository userRepository;
@BeforeEach
void setUp() {
// Initialize mocks before each test execution
MockitoAnnotations.openMocks(this);
}
@Test
@DisplayName("Should successfully load user details when user exists")
void testLoadUserByUsername_Success() {
// Arrange
User mockUser = User.builder()
.username("ram")
.password("encoded_password_123")
.roles(Collections.singletonList("USER"))
.build();
when(userRepository.findByUsername("ram")).thenReturn(mockUser);
// Act
UserDetails userDetails = userDetailsService.loadUserByUsername("ram");
// Assert
assertNotNull(userDetails);
assertEquals("ram", userDetails.getUsername());
assertEquals("encoded_password_123", userDetails.getPassword());
// Verify repository interactions
verify(userRepository, times(1)).findByUsername("ram");
}
@Test
@DisplayName("Should return user when stubbed with generic String matcher")
void testLoadUserByUsername_WithArgumentMatcher() {
// Arrange
User mockUser = User.builder()
.username("genericUser")
.password("encoded_password")
.roles(Collections.singletonList("USER"))
.build();
when(userRepository.findByUsername(anyString())).thenReturn(mockUser);
// Act
UserDetails userDetails = userDetailsService.loadUserByUsername("anyRandomName");
// Assert
assertNotNull(userDetails);
assertEquals("genericUser", userDetails.getUsername());
}
}
Summary and Key Takeaways
- Avoid Spring Context Overhead in Unit Tests: Using
@SpringBootTestfor plain business logic testing introduces unnecessary latency by spinning up component scanning and database connections. - Use
@Mockfor Pure Dependencies: Annotate required interface/class dependencies with@Mockto create lightweight simulated objects. - Use
@InjectMocksfor the Unit Under Test: Annotate your primary service class with@InjectMocksto construct its instance and automatically wire all created@Mockfields into it. - Explicit Mock Initialization: Call
MockitoAnnotations.openMocks(this)inside a@BeforeEachsetup method (or use@ExtendWith(MockitoExtension.class)) to preventNullPointerExceptions. @Mockvs@MockBeanDistinction:@Mockis managed by Mockito for fast unit tests;@MockBeanis managed by Spring'sApplicationContextfor integration tests.- Control Behavior with
when().thenReturn(): Stub return values for specific method calls or useArgumentMatchers(e.g.,anyString()) for flexible matching. - Selective Integration Testing: Reserve
@MockBean+@SpringBootTestfor scenarios where you must test real interactions with certain Spring components (e.g., Redis, messaging queues) while overriding others (e.g., database repositories).