Mockito Spy方法未调用问题解析
本文旨在解决使用Mockitospy时,被桩(stub)方法未按预期执行的问题。核心原因在于生产代码直接实例化依赖对象,而非使用测试中创建的间谍对象。文章将详细阐述这一常见误区,并提供通过依赖注入(DependencyInjection)进行解耦的解决方案,从而有效提升代码的可测试性与模块化程度。

问题剖析:Mockito Spy的常见误区
在单元测试中,Mockito 的 spy 功能允许我们对一个真实对象进行部分模拟,即可以调用真实对象的方法,也可以对特定方法进行桩(stub)或验证(verify)。然而,一个常见的误解是,当生产代码(即被测试的代码)内部直接创建了它所依赖的对象实例时,测试中创建的 spy 对象将无法生效。
考虑以下生产代码片段,其中 MyService 类直接创建了 GetOptionBidPrice 的实例:
// 生产代码
public class MyService {
public double calculateBidPrice() {
// 问题所在:MyService 内部直接实例化 GetOptionBidPrice
GetOptionBidPrice getOptionBidPrice = new GetOptionBidPrice(/* 构造参数 */);
double bidPrice = getOptionBidPrice.getBidPrice();
return bidPrice;
}
}
// 被依赖的类
public class GetOptionBidPrice {
// 假设有构造函数和getBidPrice方法
public GetOptionBidPrice(/* 构造参数 */) {
// ... 初始化 ...
}
public double getBidPrice() {
// 真实业务逻辑,可能返回0.0或其他默认值
return 0.0;
}
}在尝试测试 MyService 时,我们可能会这样编写测试代码,试图桩化 getBidPrice 方法:
// 测试代码 (错误示范)
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyServiceTest {
@Test
void testCalculateBidPriceWithSpy() {
// 1. 创建 MyService 实例
MyService myService = new MyService();
// 2. 创建 GetOptionBidPrice 的间谍对象并桩化其方法
// 注意:这里创建的 spyGetOptionBidPrice 是一个独立的实例
GetOptionBidPrice spyGetOptionBidPrice = spy(new GetOptionBidPrice(/* 构造参数 */));
doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice();
// 3. 调用 MyService 的方法
double result = myService.calculateBidPrice();
// 预期结果是 100.0,但实际结果会是 0.0
// 因为 myService 内部创建了一个全新的 GetOptionBidPrice 实例,而不是我们桩化的 spyGetOptionBidPrice
System.out.println("实际结果: " + result); // 输出 0.0
// assertEquals(100.0, result); // 断言失败
}
}问题根源: 上述测试代码的问题在于,MyService 内部的 calculateBidPrice 方法在执行时,会通过 new GetOptionBidPrice() 语句重新创建一个全新的 GetOptionBidPrice 实例。这个新实例与我们在测试中创建并桩化的 spyGetOptionBidPrice 实例是完全不同的两个对象。因此,MyService 内部调用的 getBidPrice() 方法是新实例的真实方法,而不是间谍对象上被桩化的方法,导致桩化操作未能生效。
解决方案:拥抱依赖注入
解决这一问题的核心思想是:将依赖对象的创建和管理从被依赖类中分离出来,让被依赖类不再直接负责创建其依赖。这种设计模式被称为依赖注入(Dependency Injection, DI)。通过依赖注入,我们可以在测试时注入一个 spy 或 mock 对象,而在生产环境中注入真实对象。
依赖注入有多种实现方式,最常见的是构造函数注入和方法注入。
1. 构造函数注入
这是最推荐的方式,尤其适用于强制性依赖。被依赖对象在构造时就接收其所有依赖。
改造后的生产代码:
// 生产代码 (使用构造函数注入)
public class MyService {
private final GetOptionBidPrice getOptionBidPrice;
// 依赖通过构造函数注入
public MyService(GetOptionBidPrice getOptionBidPrice) {
this.getOptionBidPrice = getOptionBidPrice;
}
public double calculateBidPrice() {
// 现在 MyService 使用的是注入进来的 GetOptionBidPrice 实例
return getOptionBidPrice.getBidPrice();
}
}改造后的测试代码:
// 测试代码 (使用构造函数注入)
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MyServiceTest {
@Test
void testCalculateBidPriceWithInjectedSpy() {
// 1. 创建 GetOptionBidPrice 的间谍对象并桩化其方法
GetOptionBidPrice spyGetOptionBidPrice = spy(new GetOptionBidPrice(/* 构造参数 */));
doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice();
// 2. 将间谍对象注入到 MyService 实例中
MyService myService = new MyService(spyGetOptionBidPrice);
// 3. 调用 MyService 的方法
double result = myService.calculateBidPrice();
// 现在,MyService 内部调用的是我们注入的间谍对象的 getBidPrice() 方法
assertEquals(100.0, result);
verify(spyGetOptionBidPrice).getBidPrice(); // 验证方法是否被调用
}
}2. 方法注入
如果依赖不是每次操作都必须的,或者需要在特定方法执行时才提供,可以使用方法注入。
改造后的生产代码:
// 生产代码 (使用方法注入)
public class MyService {
public double calculateBidPrice(GetOptionBidPrice getOptionBidPrice) {
// 依赖通过方法参数注入
return getOptionBidPrice.getBidPrice();
}
}改造后的测试代码:
// 测试代码 (使用方法注入)
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MyServiceTest {
@Test
void testCalculateBidPriceWithMethodInjectedSpy() {
// 1. 创建 GetOptionBidPrice 的间谍对象并桩化其方法
GetOptionBidPrice spyGetOptionBidPrice = spy(new GetOptionBidPrice(/* 构造参数 */));
doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice();
// 2. 创建 MyService 实例
MyService myService = new MyService();
// 3. 调用 MyService 的方法时,将间谍对象作为参数传入
double result = myService.calculateBidPrice(spyGetOptionBidPrice);
assertEquals(100.0, result);
verify(spyGetOptionBidPrice).getBidPrice();
}
}依赖注入的优势
除了解决 spy 不生效的问题,依赖注入还带来了多方面的好处:
- 提高可测试性: 模块之间的依赖关系清晰,方便在测试中替换为 mock 或 spy 对象,从而隔离测试范围,实现真正的单元测试。
- 降低耦合度: 对象不再直接负责创建和管理其依赖,而是由外部(如 IoC 容器或测试代码)提供,使得模块之间的耦合度降低,更易于维护和修改。
- 提升代码可维护性和扩展性: 当依赖发生变化时,只需修改注入点,而无需修改依赖类内部的创建逻辑。这使得系统更容易适应需求变化。
- 更好的代码结构: 强制开发者思考模块间的依赖关系,有助于设计出更清晰、更符合单一职责原则的类。
总结与最佳实践
当您在使用 Mockito 的 spy 或 mock 功能时发现桩化或模拟的行为未能按预期执行,一个首要的排查方向就是检查您的生产代码是否直接实例化了其依赖。如果是,那么依赖注入就是解决这个问题的关键。
关键点:
- 避免在被测试类内部直接 new 依赖对象。
- 优先使用构造函数注入来处理强制性依赖。
- 考虑使用方法注入处理可选或上下文相关的依赖。
- 利用 Mockito 的 @InjectMocks 和 @Mock/@Spy 注解可以简化测试代码,Mockito 会尝试自动注入依赖。
通过采纳依赖注入模式,不仅能有效解决 Mockito spy 的使用问题,更能从根本上提升代码的质量、可测试性和可维护性,是现代软件开发中不可或缺的设计实践。
WPS Office是一款面向macOS用户的综合办公套件,将文字处理、电子表格、演示文稿、PDF工具、模板与云协作整合在同一应用中。它兼容常见Microsoft Office格式,支持Intel及Apple芯片Mac,基础功能可免费使用,
Shapr3D是一款面向工业设计、机械工程、建筑概念和三维打印工作流的CAD软件。Mac版采用Parasolid建模内核,支持草图约束、实体建模、工程图、可视化渲染及常见CAD格式交换,并可通过账户在多台设备之间同步项目。
REAPER是Cockos开发的数字音频工作站,提供多轨音频与MIDI录制、剪辑、处理、混音和母带制作工具。Mac版兼容Intel与Apple芯片,支持AU、VST、VST3、CLAP等插件格式,并提供高度可定制的工作流程。
Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理工具,可在 Windows 上完成照片修饰、选区抠图、图层合成、文字排版、绘画及生成式编辑。软件采用订阅授权,由 Creative Cloud 管理安装、更新与许
Adobe InDesign 是面向印刷与数字出版的专业页面排版工具,可制作书籍、杂志、宣传册、海报、交互式 PDF 和电子书。它提供精细的文字样式、多页面管理、印前检查及多格式输出能力,并能与 Photoshop、Illustrator
Adobe Illustrator 是用于绘制矢量图形、插画、图标、徽标和版式的专业设计工具。它以路径和锚点为核心,作品放大后仍能保持清晰,并提供画板、文字、渐变、图像描摹及精确变换等功能,适合品牌视觉、印刷物料、数字界面和商业插画创作。













