Java单元测试中的Mock技术详解与实践
Mock(模拟)是一种单元测试技术,它通过创建虚拟对象来替代真实依赖项,从而使测试能够专注于当前单元的功能验证。Mock技术是现代单元测试不可或缺的工具,它通过虚拟依赖对象使测试更加专注、快速和可靠。Mockito作为Java领域最流行的Mock框架,提供了简洁而强大的API,能够满足绝大多数测试场景的需求。测试方法名应清晰表达测试意图4.避免Mock具体实现类:尽量对接口进行Mock5.及时清理
Java单元测试中的Mock技术详解与实践
文章目录
为什么需要单元测试?
在软件开发过程中,单元测试是确保代码质量的第一道防线。它是指对软件中的最小可测试单元(通常是方法或类)进行检查和验证的过程。单元测试的重要性体现在:
- 早期发现问题:在开发阶段就能发现并修复问题,降低后期修复成本
- 提高代码质量:迫使开发者编写更模块化、低耦合的代码
- 文档作用:测试用例本身就是如何使用代码的示例
- 重构保障:确保重构不会破坏现有功能
- 快速反馈:开发者可以立即知道修改是否破坏了现有功能
然而,在真实项目中,单元测试面临诸多挑战:
- 被测代码依赖外部系统(数据库、网络服务等)
- 测试环境难以搭建或维护成本高
- 某些场景难以模拟(如异常情况)
这正是Mock技术要解决的问题。
Mock技术简介
Mock(模拟)是一种单元测试技术,它通过创建虚拟对象来替代真实依赖项,从而使测试能够专注于当前单元的功能验证。
Mock的核心价值
- 隔离测试环境:消除外部依赖对测试的影响
- 模拟复杂场景:轻松构造各种测试场景(包括异常情况)
- 验证交互行为:检查被测对象是否正确调用了依赖对象
- 提升测试速度:避免真实IO操作,测试执行更快
Mock vs Stub vs Spy
- Mock:完全虚拟的对象,可以验证交互行为
- Stub:提供预设响应的简单测试替身
- Spy:部分真实对象,可以监视真实调用同时覆盖特定方法
常用Mock框架
- Mockito:Java领域最流行的Mock框架,API简洁强大
- EasyMock:较早的Mock框架,语法略显繁琐
- PowerMock:扩展Mockito/EasyMock,可以Mock静态方法等
- JMock:基于特定领域的Mock框架
本文重点介绍Mockito,因为它是目前Java生态中最主流的Mock框架。
Mockito核心功能详解
前期准备
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.28</version>
</dependency>
- 创建实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String username;
private String email;
}
- 创建interface方法
public interface AccountService {
BigDecimal getBalance(String account);
void deduct(String account, BigDecimal amount);
void add(String account, BigDecimal amount);
}
public interface TransactionLog {
void log(String sourceAccount, String targetAccount, BigDecimal amount);
}
public interface TransactionService {
boolean checkBalance(String account, BigDecimal amount);
void deductAmount(String account, BigDecimal amount);
void addAmount(String account, BigDecimal amount);
void logTransaction(String sourceAccount, String targetAccount, BigDecimal amount);
void transfer(String sourceAccount, String targetAccount, BigDecimal amount) throws RuntimeException;
}
public interface HelloService {
User profile(User user);
}
- 实现方法
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id);
}
public User createUser(String username, String email) {
User user = new User();
user.setUsername(username);
user.setEmail(email);
return userRepository.save(user);
}
public void deleteUser(Long id) {
userRepository.delete(id);
}
}
public class TransactionServiceImpl implements TransactionService {
private final AccountService accountService;
private final TransactionLog transactionLog;
public TransactionServiceImpl(AccountService accountService, TransactionLog transactionLog) {
this.accountService = accountService;
this.transactionLog = transactionLog;
}
@Override
public boolean checkBalance(String account, BigDecimal amount) {
return accountService.getBalance(account).compareTo(amount) >= 0;
}
@Override
public void deductAmount(String account, BigDecimal amount) {
accountService.deduct(account, amount);
}
@Override
public void addAmount(String account, BigDecimal amount) {
accountService.add(account, amount);
}
@Override
public void logTransaction(String sourceAccount, String targetAccount, BigDecimal amount) {
transactionLog.log(sourceAccount, targetAccount, amount);
}
@Override
public void transfer(String sourceAccount, String targetAccount, BigDecimal amount) throws RuntimeException {
if (!checkBalance(sourceAccount, amount)) {
throw new RuntimeException("Insufficient funds in source account");
}
deductAmount(sourceAccount, amount);
addAmount(targetAccount, amount);
logTransaction(sourceAccount, targetAccount, amount);
}
}
@Service
public class HelloServiceImpl implements HelloService {
// @Autowired
// private UserApiClient userApiClient;
@Override
public User profile(User user) {
// return userApiClient.profile(user);
return new User(5L, "aaa", "aaaa@qq.com");
}
public User testString(String name) {
return new User(5L, "aaa", "aaaa@qq.com");
}
}
- 单元测试
@ExtendWith(MockitoExtension.class)
public class MockTest2 {
// 使用@Mock注解创建UserRepository的mock对象
@Mock
private UserRepository userRepository;
// 使用@InjectMocks注解创建UserService实例,并自动注入mock的UserRepository
@InjectMocks
private UserService userService;
@Mock
private AccountService accountServiceMock;
@Mock
private TransactionLog transactionLogMock;
/**
* 这里需要是impl实现类,才能将上面两个mock注册进到TransactionServiceImpl
*/
@InjectMocks
private TransactionServiceImpl transactionService;
@Mock
private UserApiClient userApiClient;
@InjectMocks
private HelloServiceImpl helloService;
}
1. 基础Mock使用
1.1 创建Mock对象
// 手动创建mock对象
UserRepository mockRepo = mock(UserRepository.class);
// 使用注解创建(需要配合MockitoExtension使用)
@Mock
private UserRepository userRepository;
1.2 设置方法返回值
// 基本用法
when(mockObject.method(parameters)).thenReturn(value);
// 示例
when(userRepository.findById(1L)).thenReturn(new User(1L, "test", "test@example.com"));
// 匹配任意参数
when(userRepository.save(any(User.class))).thenReturn(savedUser);
1.3 验证方法调用
// 验证方法是否被调用
verify(mockObject).method(parameters);
// 验证调用次数
verify(mockObject, times(n)).method(parameters);
verify(mockObject, atLeastOnce()).method(parameters);
verify(mockObject, never()).method(parameters);
2. 高级Mock技术
2.1 参数捕获
@Test
void testArgumentCaptor() {
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);
// 创建参数捕获器
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
service.createUser("captured", "captured@example.com");
// 捕获并验证参数,这里可以验证是否被修改
verify(mockRepo).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
assertEquals("captured", capturedUser.getUsername());
}
2.2 验证调用顺序
@Test
void testMethodInvocationOrder() {
List<String> mockedList = mock(List.class);
mockedList.add("first");
mockedList.add("second");
mockedList.clear();
InOrder inOrder = inOrder(mockedList);
inOrder.verify(mockedList).add("first");
inOrder.verify(mockedList).add("second");
inOrder.verify(mockedList).clear();
}
2.3 模拟异常
@Test
void testThrowException() {
when(userRepository.findById(1L))
.thenThrow(new RuntimeException("Database error"));
assertThrows(RuntimeException.class, () -> {
userService.getUserById(1L);
});
}
2.4 连续调用不同返回值
@Test
void testConsecutiveCalls() {
when(mockIterator.next())
.thenReturn("first")
.thenReturn("second")
.thenReturn("more");
assertEquals("first", mockIterator.next());
assertEquals("second", mockIterator.next());
assertEquals("more", mockIterator.next());
}
2.5 部分Mock(Spy)
@Test
void testSpy() {
HelloServiceImpl spy = spy(helloService);
// 覆盖特定方法
doReturn(new User(1L, "Mocked Title", "sss"))
.when(spy).testString("12");
User mockedResult = spy.testString("12");
assertEquals("Mocked Title", mockedResult.getUsername());
// 其他方法保持真实行为
User realResult = spy.testString("456");
assertEquals("Real Title for 456", realResult.getUsername());
}
3. 依赖注入与Mock
现代Java应用通常使用依赖注入(DI)框架,Mockito可以很好地与之集成:
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
// 测试方法...
}
@InjectMocks
会自动将@Mock
标记的对象注入到被测对象中。
实战案例解析
1. 用户服务测试
@Test
void testGetUserById() {
// 准备测试数据
Long userId = 1L;
User mockUser = new User(userId, "testUser", "test@example.com");
// 设置mock行为
when(userRepository.findById(userId)).thenReturn(mockUser);
// 执行测试
User result = userService.getUserById(userId);
// 验证结果
assertEquals(mockUser, result);
// 验证交互行为
verify(userRepository).findById(userId);
}
2. 转账业务测试
@Test
public void testBankTransferOrderAndTimes() throws RuntimeException {
// 设置mock行为
when(accountServiceMock.getBalance(anyString()))
.thenReturn(BigDecimal.valueOf(200));
// 执行测试
transactionService.transfer("sourceAccount", "targetAccount",
BigDecimal.valueOf(100));
// 验证调用顺序
InOrder inOrder = inOrder(accountServiceMock, transactionLogMock);
inOrder.verify(accountServiceMock).getBalance("sourceAccount");
inOrder.verify(accountServiceMock).deduct("sourceAccount", BigDecimal.valueOf(100));
inOrder.verify(accountServiceMock).add("targetAccount", BigDecimal.valueOf(100));
inOrder.verify(transactionLogMock).log("sourceAccount", "targetAccount",
BigDecimal.valueOf(100));
// 验证调用次数
verify(accountServiceMock, times(1)).getBalance(anyString());
verify(accountServiceMock, times(1)).deduct(anyString(), any());
verify(accountServiceMock, times(1)).add(anyString(), any());
verify(transactionLogMock, times(1)).log(any(), any(), any());
}
完整示例
@ExtendWith(MockitoExtension.class)
public class MockTest2 {
// 使用@Mock注解创建UserRepository的mock对象
@Mock
private UserRepository userRepository;
// 使用@InjectMocks注解创建UserService实例,并自动注入mock的UserRepository
@InjectMocks
private UserService userService;
@Mock
private AccountService accountServiceMock;
@Mock
private TransactionLog transactionLogMock;
/**
* 这里需要是impl实现类,才能将上面两个mock注册进到TransactionServiceImpl
*/
@InjectMocks
private TransactionServiceImpl transactionService;
@Mock
private UserApiClient userApiClient;
@InjectMocks
private HelloServiceImpl helloService;
@Test
public void userProfile() throws Exception {
User user = new User();
user.setUsername("123");
user.setEmail("1111");
when(userApiClient.profile(any())).thenReturn(user);
User userReturn = helloService.profile(new User());
System.out.println(JSONUtil.toJsonStr(userReturn));
}
@Test
void testGetUserById() {
// 准备测试数据
Long userId = 1L;
User mockUser = new User(userId, "testUser", "test@example.com");
// 设置mock行为:当调用findById方法并传入userId时,返回mockUser
// when(...).thenReturn(...) 是Mockito设置方法返回值的标准写法
when(userRepository.findById(userId)).thenReturn(mockUser);
// 执行测试:调用被测方法
User result = userService.getUserById(userId);
// User result2 = userService.getUserById(userId);
// 验证结果:确认返回的对象与预期一致
assertEquals(mockUser, result);
// 验证交互行为:确认在本方法里面userRepository的findById方法被调用了一次
// verify用于验证mock对象的方法调用情况
// times(1)表示预期调用1次,这也是默认值,可以省略
// times(n) - 验证方法被调用恰好n次
// verify(mock, times(1)).method() (可以简写为 verify(mock).method())
// verify(mock, times(3)).method()
// atLeastOnce() - 验证方法至少被调用一次
// verify(mock, atLeastOnce()).method()
// atLeast(n) - 验证方法至少被调用n次
// verify(mock, atLeast(3)).method()
// atMostOnce() - 验证方法最多被调用一次
// verify(mock, atMostOnce()).method()
// atMost(n) - 验证方法最多被调用n次
// verify(mock, atMost(5)).method()
// never() - 验证方法从未被调用
// verify(mock, never()).method()
verify(userRepository, times(1)).findById(userId);
}
@Test
void testCreateUser() {
// 准备测试数据
String username = "newUser";
String email = "new@example.com";
User savedUser = new User(1L, username, email);
// 设置mock行为:当save方法被调用时,无论传入什么User对象都返回savedUser
// any(User.class) 是参数匹配器,表示匹配任何User类型的参数
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// 执行测试
User result = userService.createUser(username, email);
// 验证结果
assertEquals(savedUser, result);
assertEquals(username, result.getUsername());
assertEquals(email, result.getEmail());
// 验证交互行为:确认save方法被调用了一次,且参数是User类型
// any(User.class) 在这里用于匹配方法参数类型
verify(userRepository, times(1)).save(any(User.class));
}
@Test
void testDeleteUser() {
Long userId = 1L;
// 执行测试:调用删除方法
userService.deleteUser(userId);
// 验证行为:确认delete方法被调用了一次,且参数是userId
// 这里没有设置when...then...因为delete方法是void返回类型
// 对于void方法,通常只需要验证它被调用了即可
verify(userRepository, times(1)).delete(userId);
}
/**
* 验证顺序
*/
@Test
void testMethodInvocationOrderAndTimes() {
// 创建一个List接口的mock对象
List<String> mockedList = mock(List.class);
// 使用mock对象进行一系列操作
mockedList.add("first");
mockedList.add("second");
mockedList.clear();
// 创建InOrder对象来验证调用顺序
// InOrder可以确保方法按照特定顺序被调用
InOrder inOrder = inOrder(mockedList);
// 验证add("first")必须在add("second")之前被调用
// 且clear()必须在它们之后被调用
inOrder.verify(mockedList).add("first");
inOrder.verify(mockedList).add("second");
inOrder.verify(mockedList).clear();
// 验证add方法总共被调用了2次,参数可以是任意字符串
// times(2)表示预期调用2次
verify(mockedList, times(2)).add(anyString());
// 验证add("first")至少被调用了一次
// atLeastOnce()表示至少一次
verify(mockedList, atLeastOnce()).add("first");
// 验证add("third")从未被调用
// never()表示预期从未调用
verify(mockedList, never()).add("third");
}
/**
* 验证业务调用顺序
*
* @throws RuntimeException
*/
@Test
public void testBankTransferOrderAndTimes() throws RuntimeException {
when(accountServiceMock.getBalance(anyString())).thenReturn(BigDecimal.valueOf(200));
transactionService.transfer("sourceAccount", "targetAccount", BigDecimal.valueOf(100));
InOrder inOrder = inOrder(accountServiceMock, transactionLogMock);
// 排序,先获取账号余额
inOrder.verify(accountServiceMock).getBalance("sourceAccount");
// 减去花费金额
inOrder.verify(accountServiceMock).deduct("sourceAccount", BigDecimal.valueOf(100));
// 目标账户添加金额
inOrder.verify(accountServiceMock).add("targetAccount", BigDecimal.valueOf(100));
// 打印日志
inOrder.verify(transactionLogMock).log("sourceAccount", "targetAccount", BigDecimal.valueOf(100));
verify(accountServiceMock, times(1)).getBalance(anyString());
verify(accountServiceMock, times(1)).deduct(anyString(), any());
verify(accountServiceMock, times(1)).add(anyString(), any());
verify(transactionLogMock, times(1)).log(any(), any(), any());
}
/**
* 模拟抛出异常
*/
@Test
void testThrowException() {
// 手动创建UserRepository的mock对象
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);
Long userId = 1L;
// 设置当调用findById(userId)时抛出RuntimeException
// thenThrow()用于模拟方法抛出异常
when(mockRepo.findById(userId)).thenThrow(new RuntimeException("Database error"));
// 验证调用service.getUserById(userId)确实抛出了RuntimeException
// assertThrows用于验证是否抛出了预期的异常
assertThrows(RuntimeException.class, () -> {
service.getUserById(userId);
});
// 验证findById方法确实被调用了一次
verify(mockRepo).findById(userId);
}
/**
* 参数捕获
*/
@Test
void testArgumentCaptor() {
// 创建UserRepository的mock对象
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);
// 创建参数捕获器,用于捕获User类型的参数
// ArgumentCaptor可以捕获方法调用时传入的参数值
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
// 执行测试:调用创建用户方法
service.createUser("captured", "captured@example.com");
// 捕获save方法调用时传入的User参数
// verify配合captor可以捕获实际传入的参数
verify(mockRepo).save(userCaptor.capture());
// 获取捕获的参数值
User capturedUser = userCaptor.getValue();
// 验证捕获的参数值是否符合预期
assertEquals("captured", capturedUser.getUsername());
assertEquals("captured@example.com", capturedUser.getEmail());
}
/**
* 部分mock
* spy 更倾向于部分地监视一个真实的对象,只对特定的方法调用进行拦截或修改
* 这里没办法用mock,应为helloService
*/
@Test
void testSpy() {
// 这里不能用mock,因为helloservice没有被mock代理
// when(helloService.testString("123")).thenReturn(new User());
HelloServiceImpl spy = spy(helloService);
User user = new User();
user.setId(2L);
doReturn(new User(1L, "Mocked Title", "sss")).when(spy).testString("12");
User profile = spy.testString("12");
assertEquals("Mocked Title", profile.getUsername());
// 测试未被mock的方法调用是否仍然工作
user.setId(3L);
User real = spy.testString("456");
assertEquals("Real Title for 456", real.getUsername());
}
/**
* 连续调用返回不同值
*/
@Test
void testConsecutiveCalls() {
// 创建Iterator的mock对象
Iterator<String> mockIterator = mock(Iterator.class);
// 设置连续调用的不同返回值
// thenReturn可以链式调用,设置连续调用的不同返回值
when(mockIterator.next())
.thenReturn("first") // 第一次调用返回"first"
.thenReturn("second") // 第二次调用返回"second"
.thenReturn("more"); // 第三次及以后调用返回"more"
// 验证连续调用的返回值
assertEquals("first", mockIterator.next());
assertEquals("second", mockIterator.next());
assertEquals("more", mockIterator.next());
assertEquals("more", mockIterator.next()); // 继续返回"more"
}
}
Mock最佳实践
- 合理使用Mock:不要过度Mock,核心业务逻辑尽量使用真实对象
- 保持测试简洁:每个测试方法只关注一个功能点
- 命名规范:测试方法名应清晰表达测试意图
- 避免Mock具体实现类:尽量对接口进行Mock
- 及时清理:对于需要清理的Mock,使用
@AfterEach
进行重置 - 结合Spring Boot Test:对于Spring应用,可以结合
@MockBean
使用
常见问题与解决方案
- NPE问题:确保所有依赖都被Mock或注入
- 验证失败:检查调用次数和顺序是否符合预期
- 静态方法Mock:需要使用PowerMock等扩展工具
- final类/方法:Mockito 2.x+支持,可能需要额外配置
- 复杂对象构造:使用Builder模式或辅助方法简化测试数据准备
总结
Mock技术是现代单元测试不可或缺的工具,它通过虚拟依赖对象使测试更加专注、快速和可靠。Mockito作为Java领域最流行的Mock框架,提供了简洁而强大的API,能够满足绝大多数测试场景的需求。
测试方法名应清晰表达测试意图
4. 避免Mock具体实现类:尽量对接口进行Mock
5. 及时清理:对于需要清理的Mock,使用@AfterEach
进行重置
6. 结合Spring Boot Test:对于Spring应用,可以结合@MockBean
使用
常见问题与解决方案
- NPE问题:确保所有依赖都被Mock或注入
- 验证失败:检查调用次数和顺序是否符合预期
- 静态方法Mock:需要使用PowerMock等扩展工具
- final类/方法:Mockito 2.x+支持,可能需要额外配置
- 复杂对象构造:使用Builder模式或辅助方法简化测试数据准备
总结
Mock技术是现代单元测试不可或缺的工具,它通过虚拟依赖对象使测试更加专注、快速和可靠。Mockito作为Java领域最流行的Mock框架,提供了简洁而强大的API,能够满足绝大多数测试场景的需求。
掌握Mock技术不仅能提高测试代码质量,还能促使开发者思考代码的设计质量,推动实现更加松耦合、高内聚的系统架构。希望本文的详细介绍和实战案例能帮助你在项目中更好地应用Mock技术,构建更加健壮的测试体系。
更多推荐
所有评论(0)