
在使用mockito进行单元测试时,spy(监控)功能允许我们对真实对象进行部分模拟,即保留对象原有的行为,同时可以对特定方法进行桩化(stubbing)或验证(verification)。然而,一个常见的误区可能导致 spy 的桩化设置失效,使得测试代码预期获得桩化值,实际却调用了真实方法并返回其默认值或实际计算结果。
问题的核心在于,如果你的生产代码(即被测试的代码)在内部自行创建了 spy 对象所代表的类实例,那么你在测试代码中创建并桩化的 spy 实例将不会被生产代码使用。生产代码将操作一个全新的、未经桩化的实例。
考虑以下代码示例:
依赖类 (GetOptionBidPrice.java)
// 假设这是一个负责获取期权投标价格的类
public class GetOptionBidPrice {
public double getBidPrice() {
// 实际的业务逻辑,可能涉及网络请求或复杂计算
System.out.println("调用了 GetOptionBidPrice 的真实 getBidPrice() 方法");
return 0.0; // 假设真实方法默认返回0.0
}
}被测试的业务逻辑类 (MyService.java) - 错误示范
public class MyService {
// 这种方法内部直接创建 GetOptionBidPrice 实例
public double calculateTotalBidPriceProblematic() {
GetOptionBidPrice getOptionBidPrice = new GetOptionBidPrice(); // 问题所在:内部自行创建实例
double bidPrice = getOptionBidPrice.getBidPrice();
return bidPrice * 2;
}
}测试代码 (MyServiceTest.java) - 桩化失效的场景
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
public class MyServiceTest {
@Test
void testCalculateTotalBidPriceProblematic_StubbingFails() {
// 尝试对 GetOptionBidPrice 进行 spy 并桩化
GetOptionBidPrice spyGetOptionBidPrice = spy(new GetOptionBidPrice());
doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice(); // 桩化 getBidPrice 方法返回 100.0
MyService myService = new MyService();
// 调用被测试方法
double result = myService.calculateTotalBidPriceProblematic();
// 预期结果应为 100.0 * 2 = 200.0,但实际会是 0.0 * 2 = 0.0
// 因为 myService 内部创建了一个新的 GetOptionBidPrice 实例,而不是使用了 spyGetOptionBidPrice
System.out.println("实际结果: " + result);
assertEquals(0.0, result, "桩化未生效,真实方法被调用"); // 断言会失败,因为结果是0.0
verify(spyGetOptionBidPrice, never()).getBidPrice(); // 验证 spy 上的方法从未被调用
}
}在上述示例中,尽管我们在测试中对 spyGetOptionBidPrice 进行了桩化,但 MyService 内部的 calculateTotalBidPriceProblematic 方法却创建了它自己的 GetOptionBidPrice 实例。因此,MyService 操作的是一个全新的、未经桩化的对象,导致我们的桩化设置完全失效。
解决这一问题的核心思想是依赖注入(Dependency Injection, DI)。依赖注入是一种设计模式,它允许一个对象接收其所依赖的其他对象,而不是在内部自行创建这些依赖。这极大地提高了代码的模块化、可测试性和可维护性。
通过依赖注入,我们可以在测试时将我们准备好的 spy 实例“注入”到被测试的对象中,从而确保被测试对象使用的是我们期望的、已被桩化的实例。
被测试的业务逻辑类 (MyService.java) - 正确示范
public class MyService {
// 通过构造函数或方法参数注入 GetOptionBidPrice 实例
// 推荐使用构造函数注入,使依赖关系更明确
private final GetOptionBidPrice getOptionBidPrice;
// 构造函数注入
public MyService(GetOptionBidPrice getOptionBidPrice) {
this.getOptionBidPrice = getOptionBidPrice;
}
// 或者使用方法参数注入(适用于单次操作的依赖)
public double calculateTotalBidPriceCorrect(GetOptionBidPrice getOptionBidPrice) {
double bidPrice = getOptionBidPrice.getBidPrice();
return bidPrice * 2;
}
// 如果是构造函数注入,方法体如下
public double calculateTotalBidPriceUsingInjectedInstance() {
double bidPrice = this.getOptionBidPrice.getBidPrice();
return bidPrice * 2;
}
}测试代码 (MyServiceTest.java) - 桩化成功的场景
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
public class MyServiceTest {
@Test
void testCalculateTotalBidPriceCorrect_StubbingSuccess() {
// 1. 创建 GetOptionBidPrice 的真实实例(如果 spy 需要基于真实对象)
GetOptionBidPrice realGetOptionBidPrice = new GetOptionBidPrice();
// 2. 创建 spy 实例
GetOptionBidPrice spyGetOptionBidPrice = spy(realGetOptionBidPrice);
// 3. 对 spy 实例的特定方法进行桩化
doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice(); // 桩化 getBidPrice 方法返回 100.0
// 4. 实例化 MyService,并通过依赖注入传入 spy 实例
MyService myService = new MyService(spyGetOptionBidPrice); // 使用构造函数注入
// 5. 调用被测试方法
double result = myService.calculateTotalBidPriceUsingInjectedInstance();
// 6. 验证和断言
verify(spyGetOptionBidPrice, times(1)).getBidPrice(); // 验证 spy 上的 getBidPrice 方法被调用了一次
assertEquals(200.0, result, "桩化成功,结果符合预期"); // 断言成功,结果为 200.0
System.out.println("实际结果: " + result);
}
@Test
void testCalculateTotalBidPriceCorrect_MethodInjectionSuccess() {
// 1. 创建 GetOptionBidPrice 的真实实例
GetOptionBidPrice realGetOptionBidPrice = new GetOptionBidPrice();
// 2. 创建 spy 实例
GetOptionBidPrice spyGetOptionBidPrice = spy(realGetOptionBidPrice);
// 3. 对 spy 实例的特定方法进行桩化
doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice();
// 4. 实例化 MyService (此时可以无参构造,如果 MyService 还有其他依赖)
MyService myService = new MyService(new GetOptionBidPrice()); // 假设 MyService 构造函数需要一个 GetOptionBidPrice,这里传入一个普通实例
// 或者 MyService 也可以有无参构造,如果它不依赖 GetOptionBidPrice 除非通过方法注入
// 5. 调用被测试方法,并通过方法参数注入 spy 实例
double result = myService.calculateTotalBidPriceCorrect(spyGetOptionBidPrice);
// 6. 验证和断言
verify(spyGetOptionBidPrice, times(1)).getBidPrice();
assertEquals(200.0, result, "桩化成功,结果符合预期");
System.out.println("实际结果: " + result);
}
}通过依赖注入,我们成功地将测试中准备好的 spy 实例传递给了 MyService,确保了 MyService 在执行业务逻辑时,调用的是我们已经桩化的 GetOptionBidPrice 实例,从而使测试能够准确地验证预期行为。
通过理解 spy 的工作原理并结合依赖注入的最佳实践,我们可以构建出更健壮、更易于测试和维护的Java应用程序。
以上就是解决Mockito Spy在类方法中桩化失效问题:依赖注入实践指南的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号