Unit Testing Service Layer with Mockito
Overview
This skill provides patterns for unit testing @Service classes using Mockito. It covers mocking all injected dependencies, verifying business logic, testing complex workflows, argument capturing, verification patterns, and testing async/reactive services without starting the Spring container.
When to Use
Use this skill when:
-
Testing business logic in @Service classes
-
Mocking repository and external client dependencies
-
Verifying service interactions with mocked collaborators
-
Testing complex workflows and orchestration logic
-
Want fast, isolated unit tests (no database, no API calls)
-
Testing error handling and edge cases in services
Instructions
Follow these steps to test service layer with Mockito:
- Add Testing Dependencies
Include JUnit 5, Mockito, and AssertJ in your test classpath.
- Create Test Class with Mockito Extension
Use @ExtendWith(MockitoExtension.class) to enable Mockito annotations.
- Declare Mocks and Service Under Test
Use @Mock for dependencies and @InjectMocks for the service being tested.
- Arrange Test Data
Create test data objects and configure mock return values using when().thenReturn().
- Execute Service Method
Call the service method being tested with test inputs.
- Assert Results
Verify the returned value using AssertJ assertions and verify mock interactions.
- Test Exception Scenarios
Configure mocks to throw exceptions and verify error handling.
Examples
Setup with Mockito and JUnit 5
Maven
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <scope>test</scope> </dependency>
Gradle
dependencies { testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core") testImplementation("org.mockito:mockito-junit-jupiter") testImplementation("org.assertj:assertj-core") }
Basic Pattern: Service with Mocked Dependencies
Single Dependency
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.assertj.core.api.Assertions.;
@ExtendWith(MockitoExtension.class) class UserServiceTest {
@Mock private UserRepository userRepository;
@InjectMocks private UserService userService;
@Test void shouldReturnAllUsers() { // Arrange List<User> expectedUsers = List.of( new User(1L, "Alice"), new User(2L, "Bob") ); when(userRepository.findAll()).thenReturn(expectedUsers);
// Act
List<User> result = userService.getAllUsers();
// Assert
assertThat(result).hasSize(2);
assertThat(result).containsExactly(
new User(1L, "Alice"),
new User(2L, "Bob")
);
verify(userRepository, times(1)).findAll();
} }
Multiple Dependencies
@ExtendWith(MockitoExtension.class) class UserEnrichmentServiceTest {
@Mock private UserRepository userRepository;
@Mock private EmailService emailService;
@Mock private AnalyticsClient analyticsClient;
@InjectMocks private UserEnrichmentService enrichmentService;
@Test void shouldCreateUserAndSendWelcomeEmail() { User newUser = new User(1L, "Alice", "alice@example.com"); when(userRepository.save(any(User.class))).thenReturn(newUser); doNothing().when(emailService).sendWelcomeEmail(newUser.getEmail());
User result = enrichmentService.registerNewUser("Alice", "alice@example.com");
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getName()).isEqualTo("Alice");
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail("alice@example.com");
verify(analyticsClient, never()).trackUserRegistration(any());
} }
Testing Exception Handling
Service Throws Expected Exception
@Test void shouldThrowExceptionWhenUserNotFound() { when(userRepository.findById(999L)) .thenThrow(new UserNotFoundException("User not found"));
assertThatThrownBy(() -> userService.getUserDetails(999L)) .isInstanceOf(UserNotFoundException.class) .hasMessageContaining("User not found");
verify(userRepository).findById(999L); }
@Test void shouldRethrowRepositoryException() { when(userRepository.findAll()) .thenThrow(new DataAccessException("Database connection failed"));
assertThatThrownBy(() -> userService.getAllUsers()) .isInstanceOf(DataAccessException.class) .hasMessageContaining("Database connection failed"); }
Testing Complex Workflows
Multiple Service Method Calls
@Test void shouldTransferMoneyBetweenAccounts() { Account fromAccount = new Account(1L, 1000.0); Account toAccount = new Account(2L, 500.0);
when(accountRepository.findById(1L)).thenReturn(Optional.of(fromAccount)); when(accountRepository.findById(2L)).thenReturn(Optional.of(toAccount)); when(accountRepository.save(any(Account.class))) .thenAnswer(invocation -> invocation.getArgument(0));
moneyTransferService.transfer(1L, 2L, 200.0);
// Verify both accounts were updated verify(accountRepository, times(2)).save(any(Account.class)); assertThat(fromAccount.getBalance()).isEqualTo(800.0); assertThat(toAccount.getBalance()).isEqualTo(700.0); }
Argument Capturing and Verification
Capture Arguments Passed to Mock
import org.mockito.ArgumentCaptor;
@Test void shouldCaptureUserDataWhenSaving() { ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class); when(userRepository.save(any(User.class))) .thenAnswer(invocation -> invocation.getArgument(0));
userService.createUser("Alice", "alice@example.com");
verify(userRepository).save(userCaptor.capture()); User capturedUser = userCaptor.getValue();
assertThat(capturedUser.getName()).isEqualTo("Alice"); assertThat(capturedUser.getEmail()).isEqualTo("alice@example.com"); }
@Test void shouldCaptureMultipleArgumentsAcrossMultipleCalls() { ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
userService.createUser("Alice", "alice@example.com"); userService.createUser("Bob", "bob@example.com");
verify(userRepository, times(2)).save(userCaptor.capture());
List<User> capturedUsers = userCaptor.getAllValues(); assertThat(capturedUsers).hasSize(2); assertThat(capturedUsers.get(0).getName()).isEqualTo("Alice"); assertThat(capturedUsers.get(1).getName()).isEqualTo("Bob"); }
Verification Patterns
Verify Call Order and Frequency
import org.mockito.InOrder;
@Test void shouldCallMethodsInCorrectOrder() { InOrder inOrder = inOrder(userRepository, emailService);
userService.registerNewUser("Alice", "alice@example.com");
inOrder.verify(userRepository).save(any(User.class)); inOrder.verify(emailService).sendWelcomeEmail(any()); }
@Test void shouldCallMethodExactlyOnce() { userService.getUserDetails(1L);
verify(userRepository, times(1)).findById(1L); verify(userRepository, never()).findAll(); }
Testing Async/Reactive Services
Service with CompletableFuture
@Test void shouldReturnCompletableFutureWhenFetchingAsyncData() { List<User> users = List.of(new User(1L, "Alice")); when(userRepository.findAllAsync()) .thenReturn(CompletableFuture.completedFuture(users));
CompletableFuture<List<User>> result = userService.getAllUsersAsync();
assertThat(result).isCompletedWithValue(users); }
Examples
Input: Service Without Test Coverage
@Service public class UserService { private final UserRepository userRepository;
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
}
Output: Service With Complete Test Coverage
@ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldReturnUserWhenFound() {
User expectedUser = new User(1L, "Alice");
when(userRepository.findById(1L)).thenReturn(Optional.of(expectedUser));
User result = userService.getUser(1L);
assertThat(result).isEqualTo(expectedUser);
verify(userRepository).findById(1L);
}
@Test
void shouldThrowExceptionWhenNotFound() {
when(userRepository.findById(999L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> userService.getUser(999L))
.isInstanceOf(UserNotFoundException.class);
}
}
Input: Manual Mock Creation (Anti-Pattern)
UserService service = new UserService(new FakeUserRepository());
Output: Mockito-Based Test
@Mock private UserRepository userRepository;
@InjectMocks private UserService userService;
@Test void test() { when(userRepository.findById(1L)).thenReturn(Optional.of(user)); // Test logic }
Best Practices
-
Use @ExtendWith(MockitoExtension.class) for JUnit 5 integration
-
Construct service manually instead of using reflection when possible
-
Mock only direct dependencies of the service under test
-
Verify interactions to ensure correct collaboration
-
Use descriptive variable names: expectedUser , actualUser , captor
-
Test one behavior per test method - keep tests focused
-
Avoid testing framework code - focus on business logic
Common Patterns
Partial Mock with Spy:
@Spy @InjectMocks private UserService userService; // Real instance, but can stub some methods
@Test void shouldUseRealMethodButMockDependency() { when(userRepository.findById(any())).thenReturn(Optional.of(new User())); // Calls real userService methods but userRepository is mocked }
Constructor Injection for Testing:
// In your service (production code) public class UserService { private final UserRepository userRepository;
public UserService(UserRepository userRepository) { this.repository = userRepository; } }
// In your test - can inject mocks directly @Test void test() { UserRepository mockRepo = mock(UserRepository.class); UserService service = new UserService(mockRepo); }
Troubleshooting
UnfinishedStubbingException: Ensure all when() calls are completed with thenReturn() , thenThrow() , or thenAnswer() .
UnnecessaryStubbingException: Remove unused stub definitions. Use @ExtendWith(MockitoExtension.class) with MockitoExtension.LENIENT if you intentionally have unused stubs.
NullPointerException in test: Verify @InjectMocks correctly injects all mocked dependencies into the service constructor.
Constraints and Warnings
-
Do not mock value objects or DTOs; create real instances with test data.
-
Avoid mocking too many dependencies; consider refactoring if a service has too many collaborators.
-
Tests should not rely on execution order; each test must be independent.
-
Be cautious with @Spy as it can lead to partial mocking which is harder to understand.
-
Mock static methods with caution using Mockito-Inline; it can cause memory leaks in long-running test suites.
-
Do not test private methods directly; test them through public method behavior.
-
Argument matchers (any() , eq() ) cannot be mixed with actual values in the same stub.
-
Avoid over-verifying; verify only interactions that are important to the test scenario.
References
-
Mockito Documentation
-
JUnit 5 User Guide
-
AssertJ Assertions