0

0

Mockito Spy失效问题解析:如何通过依赖注入确保测试有效性

聖光之護

聖光之護

发布时间:2025-08-04 19:22:15

|

326人浏览过

|

来源于php中文网

原创

Mockito Spy失效问题解析:如何通过依赖注入确保测试有效性

本文旨在解决Mockito Spy在测试中遇到的常见问题:当生产代码自行创建对象实例时,Spy的桩值无法生效。核心原因是测试代码中的Spy实例未被生产代码使用。解决方案是采用依赖注入模式,将依赖对象作为参数传递,而非在方法内部创建,从而确保测试中可以传入Spy实例,实现桩值的有效应用,提高代码可测试性。

理解Mockito Spy及其使用挑战

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)。

解决方案:依赖注入(Dependency Injection)

要解决上述问题,核心思想是确保生产代码使用的是测试中创建的spy实例,而不是自己创建新的实例。实现这一目标的标准模式是依赖注入(Dependency Injection, DI)

依赖注入是一种设计模式,它将对象所依赖的其他对象的创建和管理职责从对象本身移除,转移到外部。这意味着一个对象不再负责创建其依赖项,而是由外部(通常是框架或测试代码)提供这些依赖项。

通过依赖注入,我们可以将GetOptionBidPrice实例作为参数传递给SomeService的方法,或者通过构造函数注入到SomeService中。

STORYD
STORYD

帮你写出让领导满意的精美文稿

下载

1. 重构生产代码

修改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;
    }
}

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);
    }
}

依赖注入的优势与注意事项

  1. 提高可测试性: 依赖注入是实现高可测试性代码的关键。通过注入依赖,我们可以轻松地在测试中使用模拟或桩化对象,从而隔离被测试单元,使其不依赖于外部系统的真实行为。
  2. 降低耦合度: 对象不再硬编码其依赖项的创建过程,而是通过外部提供,这降低了模块间的耦合度,使得代码更易于维护和扩展。
  3. 遵循单一职责原则: 一个类专注于其核心业务逻辑,而不必关心其依赖项的创建和生命周期管理。
  4. 灵活性: 相同的业务逻辑可以在不同的环境中(例如,开发、测试、生产)使用不同的依赖实现。

注意事项:

  • 选择合适的注入方式: 构造函数注入是推荐的注入方式,因为它强制依赖项在对象创建时就必须提供,从而保证了对象处于有效状态。方法注入适用于可选依赖或在特定操作中才需要的依赖。
  • 避免过度注入: 如果一个类的构造函数或方法需要注入过多的依赖项,这可能是一个代码异味,表明该类承担了过多的职责,可能需要重构。
  • 结合DI框架: 在大型项目中,手动管理依赖注入会变得复杂。Spring、Guice等DI框架可以自动化依赖的创建、配置和注入过程,大大简化了开发。

总结

当Mockito spy的桩值未生效时,几乎总是因为生产代码在内部自行创建了依赖对象的新实例,而不是使用了测试中准备好的spy实例。解决此问题的根本方法是采用依赖注入模式,将依赖对象作为参数传递或通过构造函数注入,确保生产代码和测试代码操作的是同一个(无论是真实还是桩化的)对象实例。掌握依赖注入不仅能解决Mockito spy失效的问题,更是编写高质量、可测试、可维护代码的基石。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

825

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

724

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

728

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

395

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

398

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

445

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

428

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16861

2023.08.03

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

7

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
10分钟--Midjourney创作自己的漫画
10分钟--Midjourney创作自己的漫画

共1课时 | 0.1万人学习

Midjourney 关键词系列整合
Midjourney 关键词系列整合

共13课时 | 0.9万人学习

AI绘画教程
AI绘画教程

共2课时 | 0.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号