Mock技术文档

Mock技术文档

一、Mock简介

1.1 什么是Mock

Mock(模拟)是一种测试技术,用于创建虚拟对象来模拟真实对象的行为。在单元测试中,Mock对象可以替代真实依赖,使测试更加独立、可控和快速。

1.2 Mock的核心概念

  • Mock对象:模拟真实对象的虚拟对象,可以预设其行为
  • Stub(桩):预定义返回值的Mock对象
  • Spy(间谍):部分Mock,保留真实对象的部分行为
  • Verify(验证):验证Mock对象的方法是否被调用

1.3 Mock的优势

  1. 隔离性:测试不依赖外部系统(数据库、网络、第三方服务)
  2. 可控性:可以模拟各种场景(正常、异常、边界情况)
  3. 快速性:避免真实IO操作,测试执行更快
  4. 稳定性:不受外部环境变化影响
  5. 可重复性:测试结果可重复,便于CI/CD集成

二、应用场景

2.1 单元测试场景

2.1.1 依赖外部服务
  • 第三方API调用:支付接口、短信服务、地图服务等
  • 微服务调用:其他服务的RPC/HTTP调用
  • 消息队列:Kafka、RocketMQ等消息中间件
2.1.2 依赖数据库
  • 数据持久化:MyBatis、JPA等ORM框架
  • 缓存操作:Redis、Memcached等
  • 文件系统:文件读写操作
2.1.3 依赖系统资源
  • 时间相关System.currentTimeMillis()new Date()
  • 随机数RandomUUID
  • 环境变量:系统配置、环境参数

2.2 集成测试场景

  • 服务降级测试:模拟下游服务不可用
  • 性能测试:模拟高并发场景
  • 异常场景测试:模拟网络超时、服务异常

2.3 开发阶段场景

  • 并行开发:依赖模块未完成时,使用Mock进行开发
  • 接口联调:前端开发时Mock后端接口
  • 演示环境:使用Mock数据展示功能

三、在实际项目中的应用

3.1 项目中的Mock应用示例

基于当前项目(Spring Boot + MyBatis Plus),以下是常见的Mock应用场景:

3.1.1 Mock第三方服务调用
@RunWith(SpringRunner.class)
@SpringBootTest
public class AbilityPlatformServiceTest {
  
    @MockBean
    private AbilityPlatformService abilityPlatformService;
  
    @Autowired
    private YourService yourService;
  
    @Test
    public void testServiceWithMock() {
        // Mock第三方服务返回
        HujiAndMemberVo mockVo = new HujiAndMemberVo();
        mockVo.setName("测试");
        ResultVO<List<HujiAndMemberVo>> mockResult = ResultVO.success(Arrays.asList(mockVo));
      
        when(abilityPlatformService.getHujiAndMember(anyString(), anyString()))
            .thenReturn(mockResult);
      
        // 执行测试
        YourResult result = yourService.doSomething("340121198409123439", "张雨");
      
        // 验证结果
        assertNotNull(result);
        verify(abilityPlatformService, times(1)).getHujiAndMember(anyString(), anyString());
    }
}
3.1.2 Mock数据库操作
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
  
    @MockBean
    private UserBaseMapper userBaseMapper;
  
    @Autowired
    private UserService userService;
  
    @Test
    public void testFindUser() {
        // Mock数据库查询结果
        UserBase mockUser = new UserBase();
        mockUser.setId(1L);
        mockUser.setName("测试用户");
      
        when(userBaseMapper.selectById(1L)).thenReturn(mockUser);
      
        // 执行测试
        UserBase user = userService.getUserById(1L);
      
        // 验证
        assertNotNull(user);
        assertEquals("测试用户", user.getName());
    }
}
3.1.3 Mock大数据接口(项目实际案例)

项目中已有 BmBigdataMock实体用于Mock大数据接口:

// 实际项目中的Mock应用
private Residence getMockResidence(String childIdCard, String parentIdCard) {
    LambdaQueryWrapper<BmBigdataMock> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(BmBigdataMock::getConfigKey, "getResidence");
    wrapper.eq(BmBigdataMock::getChildId, childIdCard);
    wrapper.eq(BmBigdataMock::getFatherId, parentIdCard);
  
    List<BmBigdataMock> list = bmBigdataMockMapper.selectList(wrapper);
    if (CollectionUtil.isEmpty(list)) {
        throw new BigDataRunTimeException("大数据接口异常");
    }
  
    String config = list.get(0).getConfigValue();
    Residence residence = JSONObject.parseObject(config, Residence.class);
    // 处理业务逻辑...
    return residence;
}

应用场景

  • 开发环境无法连接真实大数据服务
  • 测试环境需要可控的测试数据
  • 避免频繁调用真实接口产生费用

3.2 Spring Boot中的Mock注解

3.2.1 @MockBean vs @Mock
// @MockBean:Spring容器中的Bean,会替换真实Bean
@MockBean
private UserService userService;

// @Mock:普通Mock对象,需要手动注入
@Mock
private UserService userService;

@InjectMocks
private YourService yourService; // 自动注入Mock对象
3.2.2 @SpyBean vs @Spy
// @SpyBean:部分Mock,保留真实行为,可选择性Mock方法
@SpyBean
private UserService userService;

// @Spy:普通Spy对象
@Spy
private UserService userService = new UserServiceImpl();

3.3 常见框架对比

框架特点适用场景
Mockito简单易用,功能强大单元测试,Spring Boot项目
EasyMock早期框架,语法复杂遗留项目
PowerMock可Mock静态方法、私有方法需要Mock静态方法的场景
JMockit功能全面,语法灵活复杂测试场景

四、重难点分析

4.1 难点一:Mock静态方法

问题:Mockito默认不支持Mock静态方法

解决方案

  1. 使用PowerMock(不推荐,已停止维护)
@RunWith(PowerMockRunner.class)
@PrepareForTest({DateUtils.class})
public class StaticMethodTest {
    @Test
    public void testStaticMethod() {
        PowerMockito.mockStatic(DateUtils.class);
        when(DateUtils.getCurrentTime()).thenReturn(1234567890L);
    }
}
  1. 使用MockedStatic(Mockito 3.4+)(推荐)
@Test
void testStaticMethod() {
    try (MockedStatic<DateUtils> mockedStatic = mockStatic(DateUtils.class)) {
        mockedStatic.when(DateUtils::getCurrentTime).thenReturn(1234567890L);
        // 测试代码
    }
}
  1. 重构代码(最佳实践)
    • 将静态方法改为实例方法
    • 使用依赖注入,便于测试

4.2 难点二:Mock私有方法

问题:无法直接Mock私有方法

解决方案

  1. 测试公有方法(推荐)

    • 私有方法通常通过公有方法间接测试
    • 如果私有方法复杂,考虑提取为独立类
  2. 使用反射(不推荐)

Method method = YourClass.class.getDeclaredMethod("privateMethod", String.class);
method.setAccessible(true);
Object result = method.invoke(object, "param");
  1. 使用PowerMock(不推荐)

4.3 难点三:Mock final类和final方法

问题:Mockito无法Mock final类和方法

解决方案

  1. 使用Mockito 2.1+:支持Mock final类(需要配置)
// 在src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
// 创建文件,内容:mock-maker-inline
  1. 重构代码:避免使用final(如果可能)

4.4 难点四:Mock链式调用

问题:Mock对象的方法返回自身,形成链式调用

解决方案

// 错误示例
when(mockObject.method1().method2()).thenReturn(result);

// 正确示例
when(mockObject.method1()).thenReturn(mockObject);
when(mockObject.method2()).thenReturn(result);

// 或者使用链式Mock
when(mockObject.method1().method2()).thenReturn(result); // 需要配置

4.5 难点五:Mock异常场景

问题:需要测试异常情况

解决方案

// Mock抛出异常
when(service.method()).thenThrow(new RuntimeException("测试异常"));

// Mock多次调用返回不同结果
when(service.method())
    .thenReturn(result1)
    .thenReturn(result2)
    .thenThrow(new RuntimeException());

// Mock void方法抛出异常
doThrow(new RuntimeException()).when(service).voidMethod();

4.6 难点六:Mock Spring Bean的循环依赖

问题:Spring Bean之间存在循环依赖,Mock时出现问题

解决方案

  1. 使用@MockBean替换整个Bean
@MockBean
private ServiceA serviceA; // 会替换Spring容器中的Bean
  1. 使用@Primary + @MockBean
@Primary
@MockBean
private ServiceA serviceA;
  1. 重构代码:消除循环依赖(最佳实践)

五、使用技巧

5.1 最佳实践

5.1.1 使用BDD风格
// Given-When-Then模式
@Test
public void testUserService() {
    // Given:准备测试数据
    User user = new User();
    user.setId(1L);
    when(userRepository.findById(1L)).thenReturn(Optional.of(user));
  
    // When:执行被测试方法
    User result = userService.getUser(1L);
  
    // Then:验证结果
    assertNotNull(result);
    assertEquals(1L, result.getId());
    verify(userRepository, times(1)).findById(1L);
}
5.1.2 使用ArgumentMatchers
// 精确匹配
when(service.method("exact")).thenReturn(result);

// 任意参数
when(service.method(anyString())).thenReturn(result);

// 自定义匹配器
when(service.method(argThat(arg -> arg.length() > 5))).thenReturn(result);

// 多个参数
when(service.method(anyString(), anyInt(), isNull())).thenReturn(result);
5.1.3 验证方法调用
// 验证调用次数
verify(service, times(1)).method();
verify(service, never()).method();
verify(service, atLeast(1)).method();
verify(service, atMost(3)).method();

// 验证调用顺序
InOrder inOrder = inOrder(service1, service2);
inOrder.verify(service1).method1();
inOrder.verify(service2).method2();

// 验证无其他交互
verifyNoMoreInteractions(service);
verifyZeroInteractions(service);
5.1.4 使用Answer处理复杂逻辑
when(service.method(anyString())).thenAnswer(invocation -> {
    String param = invocation.getArgument(0);
    // 根据参数返回不同结果
    if (param.startsWith("test")) {
        return "test result";
    }
    return "default result";
});

5.2 常见陷阱与避免

5.2.1 过度Mock

问题:Mock了不应该Mock的对象

解决

  • 只Mock外部依赖(数据库、网络、文件系统)
  • 不Mock值对象(DTO、VO)
  • 不Mock简单工具类
5.2.2 Mock验证过于严格

问题:验证了不必要的调用细节

解决

  • 只验证关键业务逻辑
  • 使用 atLeast()atMost()而不是 times()
  • 避免验证内部实现细节
5.2.3 忘记重置Mock

问题:测试之间Mock状态污染

解决

@Before
public void setUp() {
    reset(mockObject); // 重置Mock
}
5.2.4 Mock真实对象

问题:Mock了不应该Mock的真实对象

解决

  • 使用 @Spy@SpyBean保留部分真实行为
  • 明确区分Mock和真实对象的使用场景

5.3 性能优化技巧

5.3.1 使用@MockitoSettings
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class TestClass {
    // 宽松模式,未使用的Mock不会报错
}
5.3.2 批量Mock
@Mock
private Service1 service1;
@Mock
private Service2 service2;
@Mock
private Service3 service3;

// 使用@InjectMocks自动注入
@InjectMocks
private YourService yourService;

5.4 调试技巧

5.4.1 打印Mock调用信息
// 使用ArgumentCaptor捕获参数
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
verify(service).method(captor.capture());
System.out.println("Captured: " + captor.getValue());
5.4.2 使用Mockito的调试模式
// 启用详细日志
Mockito.debug();
// 或使用Mockito.withSettings().verboseLogging()

六、小结

6.1 Mock技术总结

Mock技术是现代软件开发中不可或缺的测试工具,它能够:

  1. 提高测试效率:快速执行,不依赖外部环境
  2. 增强测试覆盖:轻松模拟各种异常和边界情况
  3. 改善代码质量:促进依赖注入和接口设计
  4. 加速开发流程:支持并行开发和持续集成

6.2 适用原则

  • 应该Mock:外部服务、数据库、文件系统、网络请求
  • 可以Mock:复杂业务逻辑、耗时操作、随机数生成
  • 不应该Mock:值对象、简单工具类、被测试类本身

6.3 学习路径建议

  1. 初级阶段:掌握 @Mock@MockBeanwhen().thenReturn()
  2. 中级阶段:学习 ArgumentMatchersverify()、异常Mock
  3. 高级阶段:掌握 AnswerArgumentCaptor、静态方法Mock
  4. 专家阶段:理解Mock原理,能够解决复杂场景问题

6.4 项目实践建议

基于当前项目(Spring Boot + MyBatis Plus),建议:

  1. 统一Mock框架:使用Mockito作为主要Mock框架
  2. 建立Mock规范:制定团队Mock使用规范
  3. Mock数据管理:对于大数据接口等,使用数据库存储Mock数据(如 BmBigdataMock
  4. 测试覆盖率:结合JaCoCo等工具,确保测试覆盖率
  5. 持续改进:定期Review测试代码,优化Mock使用

6.5 参考资料


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值