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 的使用问题,更能从根本上提升代码的质量、可测试性和可维护性,是现代软件开发中不可或缺的设计实践。


































