google-site-verification: google61fe8ba583a51912.html
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