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

RecoveryFox AI
RecoveryFox AI

AI驱动的数据恢复、文件恢复工具

下载

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失效的问题,更是编写高质量、可测试、可维护代码的基石。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
spring框架介绍
spring框架介绍

本专题整合了spring框架相关内容,想了解更多详细内容,请阅读专题下面的文章。

114

2025.08.06

Java Spring Security 与认证授权
Java Spring Security 与认证授权

本专题系统讲解 Java Spring Security 框架在认证与授权中的应用,涵盖用户身份验证、权限控制、JWT与OAuth2实现、跨站请求伪造(CSRF)防护、会话管理与安全漏洞防范。通过实际项目案例,帮助学习者掌握如何 使用 Spring Security 实现高安全性认证与授权机制,提升 Web 应用的安全性与用户数据保护。

29

2026.01.26

PHP 命令行脚本与自动化任务开发
PHP 命令行脚本与自动化任务开发

本专题系统讲解 PHP 在命令行环境(CLI)下的开发与应用,内容涵盖 PHP CLI 基础、参数解析、文件与目录操作、日志输出、异常处理,以及与 Linux 定时任务(Cron)的结合使用。通过实战示例,帮助开发者掌握使用 PHP 构建 自动化脚本、批处理工具与后台任务程序 的能力。

41

2025.12.13

俄罗斯Yandex引擎入口
俄罗斯Yandex引擎入口

2026年俄罗斯Yandex搜索引擎最新入口汇总,涵盖免登录、多语言支持、无广告视频播放及本地化服务等核心功能。阅读专题下面的文章了解更多详细内容。

165

2026.01.28

包子漫画在线官方入口大全
包子漫画在线官方入口大全

本合集汇总了包子漫画2026最新官方在线观看入口,涵盖备用域名、正版无广告链接及多端适配地址,助你畅享12700+高清漫画资源。阅读专题下面的文章了解更多详细内容。

34

2026.01.28

ao3中文版官网地址大全
ao3中文版官网地址大全

AO3最新中文版官网入口合集,汇总2026年主站及国内优化镜像链接,支持简体中文界面、无广告阅读与多设备同步。阅读专题下面的文章了解更多详细内容。

73

2026.01.28

php怎么写接口教程
php怎么写接口教程

本合集涵盖PHP接口开发基础、RESTful API设计、数据交互与安全处理等实用教程,助你快速掌握PHP接口编写技巧。阅读专题下面的文章了解更多详细内容。

2

2026.01.28

php中文乱码如何解决
php中文乱码如何解决

本文整理了php中文乱码如何解决及解决方法,阅读节专题下面的文章了解更多详细内容。

4

2026.01.28

Java 消息队列与异步架构实战
Java 消息队列与异步架构实战

本专题系统讲解 Java 在消息队列与异步系统架构中的核心应用,涵盖消息队列基本原理、Kafka 与 RabbitMQ 的使用场景对比、生产者与消费者模型、消息可靠性与顺序性保障、重复消费与幂等处理,以及在高并发系统中的异步解耦设计。通过实战案例,帮助学习者掌握 使用 Java 构建高吞吐、高可靠异步消息系统的完整思路。

8

2026.01.28

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
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号