在单元测试中,被测试的代码往往依赖其他组件返回的对象,如果直接使用真实对象,会让测试受外部组件状态影响,难以稳定复现问题,也会增加测试的执行耗时。通过解耦设计和依赖注入,再配合对象模拟技术,可以有效解决这类问题。

为什么需要模拟方法返回的对象
当被测试方法内部调用了其他服务的方法获取对象时,这些服务可能存在以下特点:
- 依赖外部系统,比如数据库、第三方接口,调用耗时久且结果不稳定
- 初始化成本高,需要复杂的配置或者环境支持
- 返回的对象状态难以控制,无法方便构造异常场景
如果直接依赖真实对象,单元测试会变成集成测试,失去单元测试快速反馈、精准定位问题的优势。而模拟方法返回的对象,可以让测试只关注当前被测试逻辑的正确性。
依赖注入实现解耦
要模拟方法返回的对象,首先需要让被测试代码的依赖可以被替换,依赖注入是实现解耦的常用方式。常见的依赖注入有构造方法注入、Setter方法注入两种形式。
构造方法注入示例
先定义一个用户服务的接口,被测试的订单服务依赖这个接口获取用户对象:
// 用户服务接口
public interface UserService {
User getUserById(Long userId);
}
// 用户实体类
public class User {
private Long id;
private String name;
// 构造方法、getter、setter省略
}
// 订单服务,通过构造方法注入UserService依赖
public class OrderService {
private final UserService userService;
public OrderService(UserService userService) {
this.userService = userService;
}
public String getOrderOwnerName(Long orderId) {
// 模拟根据订单ID查订单,这里省略订单查询逻辑,假设订单对应用户ID是1
Long userId = 1L;
User user = userService.getUserById(userId);
return user == null ? "未知用户" : user.getName();
}
}
通过构造方法注入依赖后,在单元测试中可以传入模拟的UserService实例,而不需要使用真实的实现。
使用Mockito模拟方法返回的对象
Mockito是Java生态中常用的模拟框架,可以方便模拟方法返回的对象。首先需要引入对应的依赖:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
基础模拟示例
下面编写OrderService的单元测试,模拟UserService.getUserById方法的返回对象:
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class OrderServiceTest {
@Test
public void testGetOrderOwnerName_UserExist() {
// 1. 创建UserService的模拟对象
UserService userService = Mockito.mock(UserService.class);
// 2. 模拟getUserById方法,当传入1L时返回预设的User对象
User mockUser = new User();
mockUser.setId(1L);
mockUser.setName("张三");
Mockito.when(userService.getUserById(1L)).thenReturn(mockUser);
// 3. 创建被测试的OrderService,注入模拟的UserService
OrderService orderService = new OrderService(userService);
// 4. 执行测试并验证结果
String result = orderService.getOrderOwnerName(100L);
assertEquals("张三", result);
}
@Test
public void testGetOrderOwnerName_UserNotExist() {
UserService userService = Mockito.mock(UserService.class);
// 模拟getUserById返回null
Mockito.when(userService.getUserById(1L)).thenReturn(null);
OrderService orderService = new OrderService(userService);
String result = orderService.getOrderOwnerName(100L);
assertEquals("未知用户", result);
}
}
模拟异常场景
如果需要测试方法调用依赖时抛出异常的场景,也可以很方便地模拟:
@Test
public void testGetOrderOwnerName_UserServiceThrowException() {
UserService userService = Mockito.mock(UserService.class);
// 模拟getUserById方法抛出异常
Mockito.when(userService.getUserById(1L)).thenThrow(new RuntimeException("用户服务不可用"));
OrderService orderService = new OrderService(userService);
// 这里可以根据实际代码的异常处理逻辑进行断言
try {
orderService.getOrderOwnerName(100L);
} catch (RuntimeException e) {
assertEquals("用户服务不可用", e.getMessage());
}
}
模拟对象的注意事项
- 只模拟需要控制的依赖,不要过度模拟,否则会让测试失去对真实逻辑的验证
- 模拟的行为要和真实依赖的行为保持一致,比如返回对象的字段、异常类型等
- 对于简单的值对象,如果构造成本低,也可以直接使用真实对象,不一定需要模拟
- 如果依赖的方法没有返回值,只是执行操作,可以使用
Mockito.doNothing()或者Mockito.verify()验证方法是否被调用
常见问题与解决方案
| 问题场景 | 解决方案 |
|---|---|
| 模拟的方法返回对象和预期不符 | 检查when中设置的方法参数是否和实际调用的一致,注意参数匹配规则,必要时使用Mockito.any()等匹配器 |
| 依赖注入时无法替换真实对象 | 检查依赖是否是通过硬编码初始化的,如果是需要改为注入方式,避免在被测试类中直接new依赖实例 |
| 模拟静态方法返回对象 | Mockito从3.4.0版本开始支持静态方法模拟,需要额外引入mockito-inline依赖,使用Mockito.mockStatic实现 |
通过依赖注入拆分代码依赖,再配合模拟框架模拟方法返回的对象,可以让单元测试更聚焦于当前逻辑,减少外部因素的干扰,提升测试的稳定性和执行效率。在实际开发中,可以根据项目的框架和场景选择合适的模拟方案,让单元测试真正发挥快速验证代码质量的作用。