Mastering Test Isolation: A Complete Guide to @InjectMocks and @Mock in Mockito

Unit testing in Java has been revolutionized by mocking frameworks, and Mockito stands as the industry standard. When writing unit tests, the key goal is to test components in isolation by replacing their dependencies with mock objects. The @Mock and @InjectMocks annotations provide a clean, declarative way to achieve this, making test setup more readable and maintainable.

This article explores these essential Mockito annotations, their usage patterns, and best practices for effective unit testing.


The Problem: Testing in Isolation

Consider a service class that depends on a repository:

public class UserService {
private UserRepository userRepository;
private EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public User createUser(String username, String email) {
// Business logic that uses dependencies
if (userRepository.existsByUsername(username)) {
throw new IllegalArgumentException("Username already exists");
}
User user = new User(username, email);
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmail(email);
return savedUser;
}
}

To unit test UserService, we need to:

  1. Isolate it from its real dependencies
  2. Control the behavior of those dependencies
  3. Verify interactions with dependencies

This is where @Mock and @InjectMocks come into play.


@Mock: Creating Mock Dependencies

The @Mock annotation creates a mock instance of a class or interface. Mock objects simulate the behavior of real objects but allow you to define their responses programmatically.

Key Characteristics:

  • Returns "empty" defaults (null, 0, false, empty collections)
  • Allows stubbing method calls with specific return values
  • Tracks interactions for verification
  • Lightweight and fast

@InjectMocks: Dependency Injection for Testing

The @InjectMocks annotation creates an instance of the class under test and injects the mock dependencies into it. Mockito uses several strategies to resolve dependencies:

  1. Constructor injection (preferred)
  2. Setter injection
  3. Field injection (if no constructor/setter)

Setting Up the Test Environment

Required Dependencies:

Maven:

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>

Gradle:

testImplementation 'org.mockito:mockito-core:5.8.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.8.0'

Basic Usage Examples

Example 1: Constructor Injection (Recommended)

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 UserServiceConstructorInjectionTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;  // Mockito will use constructor injection
@Test
void createUser_ShouldCreateUser_WhenUsernameIsAvailable() {
// Arrange (Given)
String username = "john_doe";
String email = "[email protected]";
User expectedUser = new User(username, email);
when(userRepository.existsByUsername(username)).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(expectedUser);
doNothing().when(emailService).sendWelcomeEmail(email);
// Act (When)
User result = userService.createUser(username, email);
// Assert (Then)
assertNotNull(result);
assertEquals(username, result.getUsername());
assertEquals(email, result.getEmail());
verify(userRepository).existsByUsername(username);
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail(email);
}
@Test
void createUser_ShouldThrowException_WhenUsernameExists() {
// Arrange
String username = "existing_user";
String email = "[email protected]";
when(userRepository.existsByUsername(username)).thenReturn(true);
// Act & Assert
assertThrows(IllegalArgumentException.class, () -> {
userService.createUser(username, email);
});
verify(userRepository).existsByUsername(username);
verify(userRepository, never()).save(any(User.class));
verify(emailService, never()).sendWelcomeEmail(anyString());
}
}

Example 2: Setter Injection

public class OrderService {
private PaymentProcessor paymentProcessor;
private InventoryService inventoryService;
// Setter injection
public void setPaymentProcessor(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public void setInventoryService(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
public boolean processOrder(Order order) {
// Business logic using dependencies
return true;
}
}
@ExtendWith(MockitoExtension.class)
class OrderServiceSetterInjectionTest {
@Mock
private PaymentProcessor paymentProcessor;
@Mock
private InventoryService inventoryService;
@InjectMocks
private OrderService orderService;  // Mockito will use setter injection
@Test
void processOrder_ShouldUseDependencies() {
// Test implementation
when(paymentProcessor.process(any())).thenReturn(true);
when(inventoryService.reserveItem(any())).thenReturn(true);
boolean result = orderService.processOrder(new Order());
assertTrue(result);
}
}

Example 3: Field Injection

public class ReportService {
@Autowired
private DataRepository dataRepository;
@Autowired
private TemplateEngine templateEngine;
public String generateReport(String reportId) {
// Uses field-injected dependencies
return "report";
}
}
@ExtendWith(MockitoExtension.class)
class ReportServiceFieldInjectionTest {
@Mock
private DataRepository dataRepository;
@Mock
private TemplateEngine templateEngine;
@InjectMocks
private ReportService reportService;  // Mockito will use field injection
@Test
void generateReport_ShouldUseDependencies() {
when(dataRepository.findById(anyString())).thenReturn(new Data());
when(templateEngine.render(any())).thenReturn("Report Content");
String result = reportService.generateReport("report-1");
assertEquals("Report Content", result);
}
}

Advanced Usage Patterns

1. Partial Mocks with @Spy

@ExtendWith(MockitoExtension.class)
class PartialMockTest {
@Mock
private ExternalService externalService;
@Spy
private Calculator calculator;  // Real object with some mocked methods
@InjectMocks
private BusinessService businessService;
@Test
void testWithPartialMock() {
// Use real implementation for some methods
doReturn(100).when(calculator).complexCalculation(anyInt());
int result = businessService.process(5);
assertEquals(100, result);
}
}

2. Multiple Dependencies with Same Type

public class NotificationService {
private SmsService primarySmsService;
private SmsService backupSmsService;
public NotificationService(SmsService primarySmsService, SmsService backupSmsService) {
this.primarySmsService = primarySmsService;
this.backupSmsService = backupSmsService;
}
}
@ExtendWith(MockitoExtension.class)
class MultipleSameTypeDependenciesTest {
@Mock
private SmsService primarySmsService;
@Mock
private SmsService backupSmsService;
@InjectMocks
private NotificationService notificationService;
@Test
void shouldUsePrimaryServiceFirst() {
when(primarySmsService.isAvailable()).thenReturn(true);
// Test that primary service is used when available
}
}

3. Custom Argument Matchers

@ExtendWith(MockitoExtension.class)
class ArgumentMatcherTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void createUser_WithComplexValidation() {
when(userRepository.existsByUsername(argThat(username -> 
username != null && username.length() >= 3)))
).thenReturn(false);
User user = userService.createUser("valid_user", "[email protected]");
assertNotNull(user);
}
}

Manual Setup (Alternative to Annotations)

While annotations are preferred, you can also set up mocks manually:

import org.junit.jupiter.api.BeforeEach;
import org.mockito.Mockito;
class ManualMockSetupTest {
private UserRepository userRepository;
private EmailService emailService;
private UserService userService;
@BeforeEach
void setUp() {
userRepository = Mockito.mock(UserRepository.class);
emailService = Mockito.mock(EmailService.class);
userService = new UserService(userRepository, emailService);
}
@Test
void manualMockTest() {
when(userRepository.existsByUsername("test")).thenReturn(false);
User result = userService.createUser("test", "[email protected]");
assertNotNull(result);
}
}

Common Pitfalls and Solutions

Pitfall 1: Forgetting @ExtendWith(MockitoExtension.class)

// WRONG - Mocks won't be initialized
class ForgotExtensionTest {
@Mock
private UserRepository userRepository;  // This will be null!
@Test
void test() {
// NullPointerException!
}
}
// CORRECT
@ExtendWith(MockitoExtension.class)
class CorrectTest {
@Mock
private UserRepository userRepository;  // Properly initialized
}

Pitfall 2: Incorrect Injection Strategy

public class ProblematicService {
private Dependency dependency;
// Private constructor - Mockito can't use it
private ProblematicService(Dependency dependency) {
this.dependency = dependency;
}
}
// Solution: Use field injection or provide package-private constructor

Pitfall 3: Over-mocking

@ExtendWith(MockitoExtension.class)
class OverMockingTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@Mock
private LogService logService;  // Do we really need to mock this?
@Mock
private ConfigService configService;  // Or this?
@InjectMocks
private UserService userService;
// Too many mocks make tests brittle and hard to maintain
}

Best Practices

1. Use Constructor Injection When Possible

// Good - Clear dependencies
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}

2. Keep Tests Focused

@Test
void createUser_ShouldSaveUser_WhenValidInput() {
// Single responsibility - test one behavior
}
@Test 
void createUser_ShouldSendEmail_AfterSuccessfulCreation() {
// Separate test for different behavior
}

3. Use Descriptive Test Names

// Good
@Test
void calculateTax_ShouldApplyHighRate_WhenIncomeExceedsThreshold()
// Avoid
@Test
void testCalculateTax1()

4. Verify Important Interactions

@Test
void shouldCallRepositoryAndEmailService() {
// Test implementation
verify(userRepository, times(1)).save(any(User.class));
verify(emailService, times(1)).sendWelcomeEmail(anyString());
}

5. Avoid Over-specification

// Too specific - brittle test
verify(userRepository, times(1)).save(
argThat(user -> 
user.getUsername().equals("john") && 
user.getEmail().equals("[email protected]")
)
);
// Better - more flexible
verify(userRepository, times(1)).save(any(User.class));

Integration with Other Testing Features

With @TestConfiguration:

@ExtendWith(MockitoExtension.class)
class SpringIntegrationTest {
@Mock
private ExternalService externalService;
@InjectMocks
private BusinessService businessService;
@TestConfiguration
static class TestConfig {
// Additional test configuration if needed
}
@Test
void testWithSpringIntegration() {
// Test implementation
}
}

With Parameterized Tests:

@ExtendWith(MockitoExtension.class)
class ParameterizedUserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@ParameterizedTest
@ValueSource(strings = {"user1", "user2", "user3"})
void createUser_ShouldWork_WithVariousUsernames(String username) {
when(userRepository.existsByUsername(username)).thenReturn(false);
User result = userService.createUser(username, "[email protected]");
assertNotNull(result);
assertEquals(username, result.getUsername());
}
}

Conclusion

@InjectMocks and @Mock are powerful annotations that make unit testing in Java more efficient and maintainable. By understanding their usage patterns and following best practices, you can:

  • Create isolated tests that focus on single components
  • Control dependency behavior with precise stubbing
  • Verify interactions between components
  • Write maintainable tests with clear setup and structure
  • Speed up test execution by avoiding real dependencies

Remember that good unit tests are not just about using the right tools, but also about following testing principles like isolation, clarity, and maintainability. The combination of Mockito's annotations with JUnit 5 provides a robust foundation for building high-quality test suites that support agile development and refactoring.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper