如何优雅地Mock同类方法?Spy函数实战解析

张开发
2026/4/14 2:44:30 15 分钟阅读

分享文章

如何优雅地Mock同类方法?Spy函数实战解析
1. 为什么我们需要Spy函数在单元测试中我们经常会遇到一个棘手的问题如何测试一个类的方法同时避免被同一个类中的其他方法干扰想象一下你正在测试一个复杂的业务逻辑但这个方法内部调用了另一个辅助方法。如果这个辅助方法本身还没有完成或者它依赖于外部服务比如数据库或网络请求那么你的测试就会变得异常困难。这时候Spy函数就像是一个隐形的助手它允许你部分Mock一个对象。也就是说你可以让某些方法保持原样而对其他方法进行Mock。这种灵活性在测试中非常有用尤其是当你只想替换掉一小部分行为而不是整个对象的时候。举个例子假设你有一个OrderService类其中有一个placeOrder方法它内部调用了validateOrder和saveOrder。如果你想测试placeOrder的逻辑但validateOrder还没有实现或者它依赖于外部验证服务这时候Spy就能派上用场。你可以Mock掉validateOrder而让saveOrder保持原样。2. Spy函数的基本用法2.1 创建Spy对象在Mockito中创建一个Spy对象非常简单。你可以使用spy()方法来包装一个真实的对象。下面是一个简单的例子// 假设我们有一个Calculator类 Calculator calculator new Calculator(); Calculator spyCalculator spy(calculator); // 现在spyCalculator就是一个被监控的对象 // 它的所有方法默认都会调用真实实现2.2 替换特定方法的行为创建Spy对象后你可以选择性地替换某些方法的行为。这是通过doReturn().when()语法实现的// 替换add方法的行为 doReturn(100).when(spyCalculator).add(anyInt(), anyInt()); // 现在调用add方法会返回100而不是执行真实逻辑 assertEquals(100, spyCalculator.add(2, 3)); // 其他方法仍然保持原样 assertEquals(5, spyCalculator.subtract(8, 3));这种部分Mock的能力正是Spy函数的精髓所在。它让你能够精确控制测试环境只替换那些你需要控制的部分而保持其他部分的真实行为。3. Spy vs Mock何时使用哪个很多开发者会困惑于何时使用Mock何时使用Spy。这里有一个简单的判断标准使用Mock当你需要完全控制一个对象的所有行为时。Mock对象的所有方法默认都是被替换的除非你明确指定它们的行为。使用Spy当你只想替换对象的某些方法而保留其他方法的真实实现时。Spy对象的所有方法默认都会调用真实实现除非你明确替换它们。在实际项目中我经常这样选择对于外部依赖如数据库访问、HTTP客户端通常使用Mock对于被测类本身的部分方法替换使用Spy4. 实战用Spy解决测试干扰问题让我们回到文章开头提到的场景。假设我们有一个UnitTestServices类其中包含两个方法queryUnitTestByContent和getTestList。我们想测试queryUnitTestByContent但不希望它调用真实的getTestList方法。4.1 原始代码分析Service public class UnitTestServices { Autowired private UnitTestRepository unitTestRepository; public MapUnitTestEntity, ListString queryUnitTestByContent(UnitTestEntity unitTestEntity, String... ids) { ListString testList getTestList(ids); UnitTestEntity content unitTestRepository.findByContent(unitTestEntity.getContent()); return new HashMapUnitTestEntity, ListString(){{put(content, testList);}}; } public ListString getTestList(String... ids) { return Arrays.asList(ids); } }4.2 测试方案实现RunWith(MockitoJUnitRunner.class) public class UnitTestServicesTest { InjectMocks private UnitTestServices unitTestServices; Mock private UnitTestRepository unitTestRepository; Test public void testQueryUnitTestByContent() { // 准备测试数据 UnitTestEntity unitTestEntity new UnitTestEntity(1, test); ListString mockList Arrays.asList(2, 3); // 创建Spy对象 UnitTestServices spyService spy(unitTestServices); // 替换getTestList方法的行为 doReturn(mockList).when(spyService).getTestList(any()); // 设置Repository的Mock行为 when(unitTestRepository.findByContent(any())).thenReturn(unitTestEntity); // 执行测试 MapUnitTestEntity, ListString result spyService.queryUnitTestByContent(unitTestEntity, 1, 2); // 验证结果 assertEquals(mockList, result.get(unitTestEntity)); } }在这个测试中我们做了以下几件事创建了一个真实的UnitTestServices实例用spy()方法创建了一个Spy对象只替换了getTestList方法的行为保持了queryUnitTestByContent方法的原始实现完全Mock了UnitTestRepository的行为这种组合使用Mock和Spy的方式让我们能够精确控制测试环境只测试我们关心的逻辑部分。5. Spy函数的常见陷阱与解决方案5.1 初始化问题一个常见的错误是在创建Spy对象时使用了未初始化的对象。例如// 错误示例 UnitTestServices spy spy(new UnitTestServices());如果UnitTestServices有自动注入的依赖如Autowired字段这样创建的对象会导致NPE。正确的做法是// 正确做法1使用InjectMocks InjectMocks UnitTestServices service; Test public void test() { UnitTestServices spy spy(service); // ... } // 正确做法2手动注入依赖 Test public void test() { UnitTestServices realService new UnitTestServices(); realService.setRepository(mockRepository); // 手动注入依赖 UnitTestServices spy spy(realService); // ... }5.2 final方法限制Mockito无法Spy或Mock final方法。如果你尝试这样做会得到一个异常。解决方案有避免在需要测试的类中使用final方法使用PowerMock等增强型Mock框架重构代码将final方法提取到单独的类中5.3 静态方法问题和final方法类似静态方法也不能被Spy。对于静态方法可以考虑使用Wrapper模式将静态调用封装起来使用PowerMock重构代码减少静态方法的使用6. 高级技巧Spy的更多用法6.1 验证方法调用除了替换方法行为Spy还可以用来验证方法是否被调用Test public void testMethodInvocation() { ListString list new ArrayList(); ListString spyList spy(list); spyList.add(one); spyList.add(two); // 验证add方法被调用了两次 verify(spyList, times(2)).add(anyString()); // 验证特定参数的调用 verify(spyList).add(one); verify(spyList).add(two); }6.2 部分Mock返回值有时候你不想完全替换方法行为只是想修改返回值。这时候可以使用doAnswerTest public void testPartialMock() { Calculator calculator new Calculator(); Calculator spyCalculator spy(calculator); doAnswer(invocation - { int a invocation.getArgument(0); int b invocation.getArgument(1); if (a 0) { return 0; // 特殊处理 } return invocation.callRealMethod(); // 其他情况调用真实方法 }).when(spyCalculator).add(anyInt(), anyInt()); assertEquals(0, spyCalculator.add(0, 5)); // 特殊处理 assertEquals(8, spyCalculator.add(3, 5)); // 真实逻辑 }6.3 结合Mock和Spy在实际项目中我经常将Mock和Spy结合使用。比如Test public void testCombination() { // 创建真实服务 OrderService orderService new OrderService(); // 创建Spy OrderService spyService spy(orderService); // Mock依赖 PaymentGateway mockGateway mock(PaymentGateway.class); when(mockGateway.process(any())).thenReturn(true); // 注入Mock依赖 spyService.setPaymentGateway(mockGateway); // 替换部分方法 doReturn(false).when(spyService).validate(any()); // 执行测试 boolean result spyService.placeOrder(new Order()); // 验证 assertFalse(result); verify(mockGateway, never()).process(any()); }这种组合使用的方式可以创建非常灵活的测试场景让你能够精确控制测试环境的每一个部分。

更多文章