unit-test-service-layer
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUnit Testing Service Layer with Mockito
使用Mockito进行服务层单元测试
Test @Service annotated classes by mocking all injected dependencies. Focus on business logic validation without starting the Spring container.
通过模拟所有注入的依赖项来测试带有@Service注解的类。无需启动Spring容器,专注于业务逻辑验证。
When to Use This Skill
何时使用此技能
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
在以下场景使用此技能:
- 测试@Service类中的业务逻辑
- 模拟Repository与外部客户端依赖项
- 验证服务与模拟协作方的交互
- 测试复杂工作流与编排逻辑
- 需要快速、隔离的单元测试(无需数据库,无需API调用)
- 测试服务中的错误处理与边缘情况
Setup with Mockito and JUnit 5
Mockito与JUnit 5的环境搭建
Maven
Maven
xml
<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>xml
<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
Gradle
kotlin
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.mockito:mockito-core")
testImplementation("org.mockito:mockito-junit-jupiter")
testImplementation("org.assertj:assertj-core")
}kotlin
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
单一依赖
java
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();
}
}java
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() {
// 准备
List<User> expectedUsers = List.of(
new User(1L, "Alice"),
new User(2L, "Bob")
);
when(userRepository.findAll()).thenReturn(expectedUsers);
// 执行
List<User> result = userService.getAllUsers();
// 断言
assertThat(result).hasSize(2);
assertThat(result).containsExactly(
new User(1L, "Alice"),
new User(2L, "Bob")
);
verify(userRepository, times(1)).findAll();
}
}Multiple Dependencies
多依赖
java
@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());
}
}java
@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
服务抛出预期异常
java
@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");
}java
@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
多服务方法调用
java
@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);
}java
@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(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
捕获传递给模拟对象的参数
java
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");
}java
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
调用顺序与次数验证
java
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();
}java
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
带有CompletableFuture的服务
java
@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);
}java
@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);
}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,actualUsercaptor - Test one behavior per test method - keep tests focused
- Avoid testing framework code - focus on business logic
- 使用@ExtendWith(MockitoExtension.class) 实现JUnit 5集成
- 手动构造服务,尽可能避免使用反射
- 仅模拟被测服务的直接依赖项
- 验证交互以确保协作正确
- 使用描述性变量名:如、
expectedUser、actualUsercaptor - 每个测试方法测试一个行为 - 保持测试聚焦
- 避免测试框架代码 - 专注于业务逻辑
Common Patterns
常见模式
Partial Mock with Spy:
java
@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:
java
// 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);
}使用Spy进行部分模拟:
java
@Spy
@InjectMocks
private UserService userService; // 真实实例,但可以存根部分方法
@Test
void shouldUseRealMethodButMockDependency() {
when(userRepository.findById(any())).thenReturn(Optional.of(new User()));
// 调用真实的userService方法,但userRepository是模拟的
}用于测试的构造函数注入:
java
// 在你的服务(生产代码)中
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.repository = userRepository;
}
}
// 在你的测试中 - 可以直接注入模拟对象
@Test
void test() {
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);
}Troubleshooting
故障排除
UnfinishedStubbingException: Ensure all calls are completed with , , or .
when()thenReturn()thenThrow()thenAnswer()UnnecessaryStubbingException: Remove unused stub definitions. Use with if you intentionally have unused stubs.
@ExtendWith(MockitoExtension.class)MockitoExtension.LENIENTNullPointerException in test: Verify correctly injects all mocked dependencies into the service constructor.
@InjectMocksUnfinishedStubbingException:确保所有调用都通过、或完成。
when()thenReturn()thenThrow()thenAnswer()UnnecessaryStubbingException:移除未使用的存根定义。如果你有意保留未使用的存根,可以结合使用。
@ExtendWith(MockitoExtension.class)MockitoExtension.LENIENT测试中出现NullPointerException:验证是否正确将所有模拟依赖项注入到服务构造函数中。
@InjectMocks