
mockito是一个流行的java单元测试框架,它允许开发者创建模拟对象(mocks)和部分模拟对象(spies)来隔离测试单元。spy与mock的区别在于,spy是对真实对象的包装,默认情况下会调用真实方法,只有在明确桩化(stub)时才会返回桩值;而mock则完全是虚构的,所有方法默认不执行任何操作,必须显式桩化。
在使用spy进行方法桩化时,一个常见的困惑是,尽管测试代码中已明确设置了桩值,但实际运行的生产代码却依然获取到真实对象的默认值或实际执行结果,而非桩定的值。这通常表现为测试不通过,因为生产代码的行为与预期不符。
让我们通过一个具体的例子来剖析这个问题。假设我们有一个GetOptionBidPrice类,其中包含一个getBidPrice()方法,我们的生产代码如下:
// 生产代码片段
public class SomeService {
public double calculateValue() {
GetOptionBidPrice getOptionBidPrice = new GetOptionBidPrice(...); // 问题所在:内部创建实例
double bidPrice = getOptionBidPrice.getBidPrice();
// ... 使用 bidPrice 进行后续计算
return bidPrice * 2; // 示例
}
}在测试中,我们可能尝试对GetOptionBidPrice进行spy并桩化其getBidPrice()方法:
// 测试代码片段
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
public class SomeServiceTest {
@Test
void testCalculateValueWithStubbedBidPrice() {
// 创建一个GetOptionBidPrice的spy对象
GetOptionBidPrice spyGetOptionBidPrice = spy(GetOptionBidPrice.class);
// 桩化getBidPrice()方法,使其返回100.0
doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice();
// 尝试测试 SomeService
SomeService someService = new SomeService(); // 创建 SomeService 实例
double result = someService.calculateValue(); // 调用待测试方法
// 预期 result 为 200.0 (100.0 * 2)
// 实际 result 可能是 0.0 (因为生产代码中 new 了一个新的 GetOptionBidPrice 实例)
// Assertions.assertEquals(200.0, result);
}
}在这个场景中,尽管我们在测试中创建了spyGetOptionBidPrice并桩化了它的getBidPrice()方法,但在SomeService的calculateValue()方法内部,却通过new GetOptionBidPrice(...)又创建了一个全新的、真实的GetOptionBidPrice实例。这意味着calculateValue()方法使用的是一个与测试中spy对象完全不同的实例。因此,spy对象上的桩化设置对生产代码没有任何影响,生产代码依然调用的是其内部新创建实例的真实方法,返回真实值(例如,如果getBidPrice()的默认实现返回0,那么就会得到0)。
要解决上述问题,核心思想是确保生产代码使用的是测试中创建的spy实例,而不是自己创建新的实例。实现这一目标的标准模式是依赖注入(Dependency Injection, DI)。
依赖注入是一种设计模式,它将对象所依赖的其他对象的创建和管理职责从对象本身移除,转移到外部。这意味着一个对象不再负责创建其依赖项,而是由外部(通常是框架或测试代码)提供这些依赖项。
通过依赖注入,我们可以将GetOptionBidPrice实例作为参数传递给SomeService的方法,或者通过构造函数注入到SomeService中。
修改SomeService,使其不再内部创建GetOptionBidPrice实例,而是通过方法参数接收:
// 重构后的生产代码片段
public class SomeService {
// 方式一:方法注入
public double calculateValue(GetOptionBidPrice getOptionBidPrice) {
double bidPrice = getOptionBidPrice.getBidPrice();
// ... 使用 bidPrice 进行后续计算
return bidPrice * 2;
}
// 方式二:构造函数注入 (更推荐,因为它明确了对象的依赖关系)
private final GetOptionBidPrice getOptionBidPrice;
public SomeService(GetOptionBidPrice getOptionBidPrice) {
this.getOptionBidPrice = getOptionBidPrice;
}
public double calculateValueViaConstructor() {
double bidPrice = getOptionBidPrice.getBidPrice();
return bidPrice * 2;
}
}现在,我们可以在测试中创建spy实例,并将其注入到SomeService中:
// 重构后的测试代码片段
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SomeServiceTest {
@Test
void testCalculateValueWithStubbedBidPrice_MethodInjection() {
// 创建一个GetOptionBidPrice的spy对象
GetOptionBidPrice spyGetOptionBidPrice = spy(GetOptionBidPrice.class);
// 桩化getBidPrice()方法,使其返回100.0
doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice();
// 创建 SomeService 实例
SomeService someService = new SomeService(null); // 如果 SomeService 只有方法注入,构造器可以传 null 或其他占位符
// 或者 SomeService 可以有无参构造器
// 调用待测试方法,并传入spy对象
double result = someService.calculateValue(spyGetOptionBidPrice);
// 验证结果
assertEquals(200.0, result, "桩化的值应被正确使用");
// 验证 getBidPrice 方法是否被调用
verify(spyGetOptionBidPrice).getBidPrice();
}
@Test
void testCalculateValueWithStubbedBidPrice_ConstructorInjection() {
// 创建一个GetOptionBidPrice的spy对象
GetOptionBidPrice spyGetOptionBidPrice = spy(GetOptionBidPrice.class);
// 桩化getBidPrice()方法,使其返回100.0
doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice();
// 创建 SomeService 实例,通过构造函数注入spy对象
SomeService someService = new SomeService(spyGetOptionBidPrice);
// 调用待测试方法
double result = someService.calculateValueViaConstructor();
// 验证结果
assertEquals(200.0, result, "桩化的值应被正确使用");
// 验证 getBidPrice 方法是否被调用
verify(spyGetOptionBidPrice).getBidPrice();
}
}生产环境中的使用:
在生产环境中,SomeService的调用方将传入真实的GetOptionBidPrice实例:
// 生产环境调用示例
public class MainApplication {
public static void main(String[] args) {
GetOptionBidPrice realGetOptionBidPrice = new GetOptionBidPrice(...); // 真实实例
SomeService someService = new SomeService(realGetOptionBidPrice); // 注入真实实例
double finalValue = someService.calculateValueViaConstructor();
System.out.println("Final calculated value: " + finalValue);
}
}注意事项:
当Mockito spy的桩值未生效时,几乎总是因为生产代码在内部自行创建了依赖对象的新实例,而不是使用了测试中准备好的spy实例。解决此问题的根本方法是采用依赖注入模式,将依赖对象作为参数传递或通过构造函数注入,确保生产代码和测试代码操作的是同一个(无论是真实还是桩化的)对象实例。掌握依赖注入不仅能解决Mockito spy失效的问题,更是编写高质量、可测试、可维护代码的基石。
以上就是Mockito Spy失效问题解析:如何通过依赖注入确保测试有效性的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号