Mockito Spy与依赖注入:解决测试中方法未调用模拟值的问题
问题分析:Mockito Spy与直接实例化冲突
在使用Mockito进行单元测试时,spy功能允许我们对真实对象进行部分模拟,即可以调用真实方法,也可以对特定方法进行桩(stubbing)。然而,一个常见的陷阱是,当生产代码(即被测试的方法)内部直接创建了它所依赖的对象实例时,测试中对该依赖对象创建的spy或mock实例将无法生效。
考虑以下场景:
原始生产代码片段:
public class MyService { public double calculatePrice() { GetOptionBidPrice getOptionBidPrice = new GetOptionBidPrice(...); // 问题所在:直接实例化 double bidPrice = getOptionBidPrice.getBidPrice(); // ... 其他业务逻辑使用 bidPrice return bidPrice * 1.1; // 示例计算 } } public class GetOptionBidPrice { // ... 构造函数及其他字段 public double getBidPrice() { // 真实业务逻辑,可能涉及网络请求或复杂计算 return 0.0; // 假设默认返回0 } }
对应的测试代码尝试:
import org.junit.jupiter.api.Test; import org.mockito.Mockito; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.junit.jupiter.api.Assertions.assertEquals; public class MyServiceTest { @Test void testCalculatePriceWithSpy() { // 尝试对 GetOptionBidPrice 进行 spy GetOptionBidPrice spyGetOptionBidPrice = spy(GetOptionBidPrice.class); doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice(); // 桩化 getBidPrice 方法 // 创建 MyService 实例并调用 MyService myService = new MyService(); double result = myService.calculatePrice(); // 在这里,内部仍然创建了新的 GetOptionBidPrice 实例 // 预期结果是基于桩化的 100.0,但实际可能基于 GetOptionBidPrice 的真实返回值 0.0 // assertEquals(110.0, result); // 期望值 } }
在这个例子中,尽管我们在测试中创建了spyGetOptionBidPrice并对其getBidPrice()方法进行了桩化,但MyService类的calculatePrice()方法内部通过new GetOptionBidPrice(...)创建了一个全新的GetOptionBidPrice实例。这个新实例与测试中创建的spy实例是完全独立的,因此calculatePrice()方法调用的是真实实例的getBidPrice()方法,而非spy实例上被桩化的方法,导致模拟值100.0并未生效,而是得到了真实方法返回的0.0。
解决方案:引入依赖注入
解决上述问题的核心思想是,不要让被测试类(MyService)在内部直接创建其依赖(GetOptionBidPrice)的实例。相反,应该通过某种方式将依赖对象“注入”到被测试类中。这种设计模式被称为依赖注入 (Dependency Injection, DI)。
通过依赖注入,我们可以在生产代码中注入真实的依赖对象,而在测试代码中注入spy或mock对象,从而实现对依赖行为的控制。
重构后的生产代码:
public class MyService { private final GetOptionBidPrice getOptionBidPrice; // 将依赖声明为成员变量 // 通过构造函数注入依赖 public MyService(GetOptionBidPrice getOptionBidPrice) { this.getOptionBidPrice = getOptionBidPrice; } public double calculatePrice() { double bidPrice = getOptionBidPrice.getBidPrice(); // 调用注入的依赖实例 // ... 其他业务逻辑使用 bidPrice return bidPrice * 1.1; } } // GetOptionBidPrice 类保持不变 public class GetOptionBidPrice { // ... 构造函数及其他字段 public double getBidPrice() { return 0.0; } }
重构后的测试代码:
import org.junit.jupiter.api.Test; import org.mockito.Mockito; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.junit.jupiter.api.Assertions.assertEquals; public class MyServiceTest { @Test void testCalculatePriceWithInjectedSpy() { // 创建并桩化 spy 实例 GetOptionBidPrice spyGetOptionBidPrice = spy(GetOptionBidPrice.class); doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice(); // 将 spy 实例注入到 MyService 中 MyService myService = new MyService(spyGetOptionBidPrice); double result = myService.calculatePrice(); // 现在,calculatePrice 会调用注入的 spy 实例的 getBidPrice() 方法 assertEquals(110.0, result, "计算结果应基于桩化的 bidPrice"); // 验证 getBidPrice 方法是否被调用 Mockito.verify(spyGetOptionBidPrice).getBidPrice(); } // 生产代码中如何使用 MyService public static void main(String[] args) { // 在生产环境中,注入真实的 GetOptionBidPrice 实例 GetOptionBidPrice realGetOptionBidPrice = new GetOptionBidPrice(/* 真实参数 */); MyService realMyService = new MyService(realGetOptionBidPrice); double productionResult = realMyService.calculatePrice(); System.out.println("生产环境计算结果: " + productionResult); } }
通过构造函数注入(这是最常见的依赖注入方式之一),MyService不再负责创建GetOptionBidPrice的实例,而是由外部提供。在测试中,我们提供了spy实例;在生产环境中,我们则提供真实的实例。这样,测试就能够完全控制MyService所依赖的GetOptionBidPrice的行为。
注意事项与总结
- 依赖注入的重要性: 依赖注入不仅解决了测试中的模拟问题,更是现代软件设计中的一个核心原则。它提高了代码的模块化、可测试性、可维护性和扩展性。当一个类需要依赖其他类时,优先考虑通过构造函数、Setter方法或接口注入这些依赖,而不是在类内部直接创建。
-
spy与mock的选择:
- mock: 通常用于完全模拟一个接口或类,其所有方法默认不执行真实逻辑,需要明确桩化。适用于测试单元与外部完全隔离的场景。
- spy: 用于对真实对象进行部分模拟,未桩化的方法会执行真实逻辑。适用于需要测试真实对象大部分功能,仅对少数难以测试或耗时的方法进行控制的场景。
- 本案例中使用spy是合适的,因为我们希望GetOptionBidPrice的大部分行为保持不变,只对getBidPrice()方法进行桩化。
- 测试的粒度: 单元测试应该关注单个单元(通常是一个类或一个方法)的行为,并将其依赖项隔离。通过模拟或间谍化依赖项,我们可以确保测试的焦点仅限于被测试单元本身,避免外部因素干扰测试结果。
- 避免在被测试方法内部创建依赖: 这是一个通用的反模式,它使得单元测试变得困难。如果一个方法内部创建了复杂的依赖对象,那么这个方法就很难被单独测试,因为它与它创建的依赖项紧密耦合。
- 验证行为: 在使用spy或mock后,除了验证返回值,还应使用Mockito.verify()来验证被模拟对象上的方法是否被正确调用,以及调用的次数和参数是否符合预期。
通过采纳依赖注入模式,并理解Mockito spy的工作原理,开发者可以构建出更健壮、更易于测试的应用程序。
以上是Mockito Spy与依赖注入:解决测试中方法未调用模拟值的问题的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undress AI Tool
免费脱衣服图片

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

要正确处理JDBC事务,必须先关闭自动提交模式,再执行多个操作,最后根据结果提交或回滚;1.调用conn.setAutoCommit(false)以开始事务;2.执行多个SQL操作,如INSERT和UPDATE;3.若所有操作成功则调用conn.commit(),若发生异常则调用conn.rollback()确保数据一致性;同时应使用try-with-resources管理资源,妥善处理异常并关闭连接,避免连接泄漏;此外建议使用连接池、设置保存点实现部分回滚,并保持事务尽可能短以提升性能。

SetupaMaven/GradleprojectwithJAX-RSdependencieslikeJersey;2.CreateaRESTresourceusingannotationssuchas@Pathand@GET;3.ConfiguretheapplicationviaApplicationsubclassorweb.xml;4.AddJacksonforJSONbindingbyincludingjersey-media-json-jackson;5.DeploytoaJakar

使用java.time包中的类替代旧的Date和Calendar类;2.通过LocalDate、LocalDateTime和LocalTime获取当前日期时间;3.使用of()方法创建特定日期时间;4.利用plus/minus方法不可变地增减时间;5.使用ZonedDateTime和ZoneId处理时区;6.通过DateTimeFormatter格式化和解析日期字符串;7.必要时通过Instant与旧日期类型兼容;现代Java中日期处理应优先使用java.timeAPI,它提供了清晰、不可变且线

前形式摄取,quarkusandmicronautleaddueTocile timeProcessingandGraalvSupport,withquarkusoftenpernperforminglightbetterine nosserless notelless centarios.2。

依赖性(di)IsadesignpatternwhereObjectsReceivedenciesenciesExtern上,推广looseSecouplingAndEaseerTestingThroughConstructor,setter,orfieldInjection.2.springfraMefringframeWorkSannotationsLikeLikeLike@component@component,@component,@service,@autowiredwithjava-service和@autowiredwithjava-ligatiredwithjava-lase-lightike

使用性能分析工具定位瓶颈,开发测试阶段用VisualVM或JProfiler,生产环境优先Async-Profiler;2.减少对象创建,复用对象、用StringBuilder替代字符串拼接、选择合适GC策略;3.优化集合使用,根据场景选型并预设初始容量;4.优化并发,使用并发集合、减少锁粒度、合理设置线程池;5.调优JVM参数,设置合理堆大小和低延迟垃圾回收器并启用GC日志;6.代码层面避免反射、用基本类型替代包装类、延迟初始化、使用final和static;7.持续性能测试与监控,结合JMH

Maven是Java项目管理和构建的标准工具,答案在于它通过pom.xml实现项目结构标准化、依赖管理、构建生命周期自动化和插件扩展;1.使用pom.xml定义groupId、artifactId、version和dependencies;2.掌握核心命令如mvnclean、compile、test、package、install和deploy;3.利用dependencyManagement和exclusions管理依赖版本与冲突;4.通过多模块项目结构组织大型应用并由父POM统一管理;5.配

TheJVMenablesJava’s"writeonce,runanywhere"capabilitybyexecutingbytecodethroughfourmaincomponents:1.TheClassLoaderSubsystemloads,links,andinitializes.classfilesusingbootstrap,extension,andapplicationclassloaders,ensuringsecureandlazyclassloa
