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

百灵大模型
百灵大模型

蚂蚁集团自研的多模态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

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

豆包大模型
豆包大模型

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

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

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

文心一言
文心一言

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

讯飞写作
讯飞写作

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

即梦AI
即梦AI

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

ChatGPT
ChatGPT

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

相关专题

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

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

163

2025.08.06

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

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

89

2026.01.26

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

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

76

2025.12.13

Python WebSocket实时通信与异步服务开发实践
Python WebSocket实时通信与异步服务开发实践

本专题聚焦 Python 在实时通信场景中的开发实践,系统讲解 WebSocket 协议原理、长连接管理、消息推送机制以及异步服务架构设计。内容包括客户端与服务端通信实现、连接稳定性优化、消息队列集成及高并发处理策略。通过完整案例,帮助开发者构建高效稳定的实时通信系统,适用于聊天应用、实时数据推送等场景。

5

2026.03.18

Java Spring Security权限控制与认证机制实战
Java Spring Security权限控制与认证机制实战

本专题围绕 Java 后端安全体系建设展开,重点讲解 Spring Security 在权限控制与认证机制中的应用实践。内容涵盖用户认证流程、权限模型设计、JWT 鉴权方案、OAuth2 集成以及接口安全防护策略。通过实际项目案例,帮助开发者构建安全可靠的后端认证体系,提升系统安全性与可扩展能力。

21

2026.03.18

抖漫入口地址合集
抖漫入口地址合集

本专题整合了抖漫入口地址相关合集,阅读专题下面的文章了解更多详细地址。

137

2026.03.17

多环境下的 Nginx 安装、结构与运维实战
多环境下的 Nginx 安装、结构与运维实战

本专题聚焦多环境下Nginx实战,详解开发、测试及生产环境的差异化安装策略与目录结构规划。深入剖析配置模块化设计、灰度发布流程及跨环境同步机制。结合监控告警、故障排查与自动化运维工具,提供全链路管理方案,助力团队构建灵活、高可用的Nginx服务体系,从容应对复杂业务场景挑战。

14

2026.03.17

PS 批量添加图片
PS 批量添加图片

本专题整合了PS批量添加图片教程合集,阅读专题下面的文章了解更多详细操作。

14

2026.03.17

Nginx 基础架构:从安装配置到系统化管理
Nginx 基础架构:从安装配置到系统化管理

本专题深入解析Nginx基础架构,涵盖从源码编译与包管理安装,到核心配置文件优化及虚拟主机部署。进一步探讨日志轮转、性能调优、高可用集群构建及自动化运维策略,助力管理员实现从单一服务搭建到企业级系统化管理的全面升级,确保Web服务高效、稳定运行。

7

2026.03.17

热门下载

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

精品课程

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

共1课时 | 0.1万人学习

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

共13课时 | 1.0万人学习

AI绘画教程
AI绘画教程

共2课时 | 0.2万人学习

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

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