0

0

解决Mockito Spy在类方法中桩化失效问题:依赖注入实践指南

花韻仙語

花韻仙語

发布时间:2025-08-04 19:42:10

|

399人浏览过

|

来源于php中文网

原创

解决Mockito Spy在类方法中桩化失效问题:依赖注入实践指南

本文旨在解决使用Mockito spy对类方法进行桩化(stubbing)时,桩化值未生效反而调用了真实方法的问题。核心原因在于生产代码直接实例化了被监控(spied)对象,而非使用测试中创建的 spy 实例。解决方案是采用依赖注入(Dependency Injection)模式,将 spy 对象作为依赖传递给生产代码,从而确保测试期间桩化行为的正确执行,提升代码的可测试性。

问题剖析:Mockito Spy桩化失效的根源

在使用mockito进行单元测试时,spy(监控)功能允许我们对真实对象进行部分模拟,即保留对象原有的行为,同时可以对特定方法进行桩化(stubbing)或验证(verification)。然而,一个常见的误区可能导致 spy 的桩化设置失效,使得测试代码预期获得桩化值,实际却调用了真实方法并返回其默认值或实际计算结果。

问题的核心在于,如果你的生产代码(即被测试的代码)在内部自行创建了 spy 对象所代表的类实例,那么你在测试代码中创建并桩化的 spy 实例将不会被生产代码使用。生产代码将操作一个全新的、未经桩化的实例。

考虑以下代码示例:

依赖类 (GetOptionBidPrice.java)

// 假设这是一个负责获取期权投标价格的类
public class GetOptionBidPrice {
    public double getBidPrice() {
        // 实际的业务逻辑,可能涉及网络请求或复杂计算
        System.out.println("调用了 GetOptionBidPrice 的真实 getBidPrice() 方法");
        return 0.0; // 假设真实方法默认返回0.0
    }
}

被测试的业务逻辑类 (MyService.java) - 错误示范

public class MyService {
    // 这种方法内部直接创建 GetOptionBidPrice 实例
    public double calculateTotalBidPriceProblematic() {
        GetOptionBidPrice getOptionBidPrice = new GetOptionBidPrice(); // 问题所在:内部自行创建实例
        double bidPrice = getOptionBidPrice.getBidPrice();
        return bidPrice * 2;
    }
}

测试代码 (MyServiceTest.java) - 桩化失效的场景

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

public class MyServiceTest {

    @Test
    void testCalculateTotalBidPriceProblematic_StubbingFails() {
        // 尝试对 GetOptionBidPrice 进行 spy 并桩化
        GetOptionBidPrice spyGetOptionBidPrice = spy(new GetOptionBidPrice());
        doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice(); // 桩化 getBidPrice 方法返回 100.0

        MyService myService = new MyService();

        // 调用被测试方法
        double result = myService.calculateTotalBidPriceProblematic(); 

        // 预期结果应为 100.0 * 2 = 200.0,但实际会是 0.0 * 2 = 0.0
        // 因为 myService 内部创建了一个新的 GetOptionBidPrice 实例,而不是使用了 spyGetOptionBidPrice
        System.out.println("实际结果: " + result);
        assertEquals(0.0, result, "桩化未生效,真实方法被调用"); // 断言会失败,因为结果是0.0
        verify(spyGetOptionBidPrice, never()).getBidPrice(); // 验证 spy 上的方法从未被调用
    }
}

在上述示例中,尽管我们在测试中对 spyGetOptionBidPrice 进行了桩化,但 MyService 内部的 calculateTotalBidPriceProblematic 方法却创建了它自己的 GetOptionBidPrice 实例。因此,MyService 操作的是一个全新的、未经桩化的对象,导致我们的桩化设置完全失效。

解决方案:拥抱依赖注入

解决这一问题的核心思想是依赖注入(Dependency Injection, DI)。依赖注入是一种设计模式,它允许一个对象接收其所依赖的其他对象,而不是在内部自行创建这些依赖。这极大地提高了代码的模块化、可测试性和可维护性。

CodeBuddy
CodeBuddy

腾讯云AI代码助手

下载

通过依赖注入,我们可以在测试时将我们准备好的 spy 实例“注入”到被测试的对象中,从而确保被测试对象使用的是我们期望的、已被桩化的实例。

被测试的业务逻辑类 (MyService.java) - 正确示范

public class MyService {
    // 通过构造函数或方法参数注入 GetOptionBidPrice 实例
    // 推荐使用构造函数注入,使依赖关系更明确
    private final GetOptionBidPrice getOptionBidPrice;

    // 构造函数注入
    public MyService(GetOptionBidPrice getOptionBidPrice) {
        this.getOptionBidPrice = getOptionBidPrice;
    }

    // 或者使用方法参数注入(适用于单次操作的依赖)
    public double calculateTotalBidPriceCorrect(GetOptionBidPrice getOptionBidPrice) {
        double bidPrice = getOptionBidPrice.getBidPrice();
        return bidPrice * 2;
    }

    // 如果是构造函数注入,方法体如下
    public double calculateTotalBidPriceUsingInjectedInstance() {
        double bidPrice = this.getOptionBidPrice.getBidPrice();
        return bidPrice * 2;
    }
}

测试代码 (MyServiceTest.java) - 桩化成功的场景

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

public class MyServiceTest {

    @Test
    void testCalculateTotalBidPriceCorrect_StubbingSuccess() {
        // 1. 创建 GetOptionBidPrice 的真实实例(如果 spy 需要基于真实对象)
        GetOptionBidPrice realGetOptionBidPrice = new GetOptionBidPrice();
        // 2. 创建 spy 实例
        GetOptionBidPrice spyGetOptionBidPrice = spy(realGetOptionBidPrice);

        // 3. 对 spy 实例的特定方法进行桩化
        doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice(); // 桩化 getBidPrice 方法返回 100.0

        // 4. 实例化 MyService,并通过依赖注入传入 spy 实例
        MyService myService = new MyService(spyGetOptionBidPrice); // 使用构造函数注入

        // 5. 调用被测试方法
        double result = myService.calculateTotalBidPriceUsingInjectedInstance(); 

        // 6. 验证和断言
        verify(spyGetOptionBidPrice, times(1)).getBidPrice(); // 验证 spy 上的 getBidPrice 方法被调用了一次
        assertEquals(200.0, result, "桩化成功,结果符合预期"); // 断言成功,结果为 200.0
        System.out.println("实际结果: " + result);
    }

    @Test
    void testCalculateTotalBidPriceCorrect_MethodInjectionSuccess() {
        // 1. 创建 GetOptionBidPrice 的真实实例
        GetOptionBidPrice realGetOptionBidPrice = new GetOptionBidPrice();
        // 2. 创建 spy 实例
        GetOptionBidPrice spyGetOptionBidPrice = spy(realGetOptionBidPrice);

        // 3. 对 spy 实例的特定方法进行桩化
        doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice();

        // 4. 实例化 MyService (此时可以无参构造,如果 MyService 还有其他依赖)
        MyService myService = new MyService(new GetOptionBidPrice()); // 假设 MyService 构造函数需要一个 GetOptionBidPrice,这里传入一个普通实例
                                                                    // 或者 MyService 也可以有无参构造,如果它不依赖 GetOptionBidPrice 除非通过方法注入

        // 5. 调用被测试方法,并通过方法参数注入 spy 实例
        double result = myService.calculateTotalBidPriceCorrect(spyGetOptionBidPrice);

        // 6. 验证和断言
        verify(spyGetOptionBidPrice, times(1)).getBidPrice();
        assertEquals(200.0, result, "桩化成功,结果符合预期");
        System.out.println("实际结果: " + result);
    }
}

通过依赖注入,我们成功地将测试中准备好的 spy 实例传递给了 MyService,确保了 MyService 在执行业务逻辑时,调用的是我们已经桩化的 GetOptionBidPrice 实例,从而使测试能够准确地验证预期行为。

注意事项与最佳实践

  1. 何时使用 spy vs mock:
    • mock: 当你需要完全模拟一个对象,不关心其真实行为,或者真实行为难以测试(如外部服务调用、数据库操作)时,使用 mock。mock 对象默认所有方法都不执行真实逻辑,并返回默认值(如 null、0、false)。
    • spy: 当你需要部分模拟一个真实对象时使用 spy。spy 对象会执行真实方法,除非你明确地桩化了特定方法。它适用于测试那些复杂对象中只有少数方法需要被控制的场景。
  2. 依赖注入的重要性:
    • 提高可测试性: 这是依赖注入最直接的优势。通过注入依赖,你可以轻松地在测试中替换真实依赖为模拟或桩化对象。
    • 降低耦合度: 对象不再负责创建其依赖,而是接收它们,这使得组件之间更加独立。
    • 提高可维护性: 依赖关系清晰,更易于理解和修改。
    • 促进测试驱动开发(TDD): 在编写代码之前考虑如何测试,自然会倾向于设计易于注入依赖的组件。
  3. 避免在生产代码中直接实例化依赖: 除非依赖是一个简单的、无状态的工具类,并且其行为在任何情况下都是确定且无副作用的,否则应尽量避免在方法或类内部直接 new 依赖对象。这会使得单元测试变得困难,因为你无法替换这些内部创建的依赖。
  4. 构造函数注入是首选: 对于一个类必需的依赖,通常推荐使用构造函数注入。这使得类的依赖关系在对象创建时就明确可见,并且保证了对象在创建后处于有效状态。

通过理解 spy 的工作原理并结合依赖注入的最佳实践,我们可以构建出更健壮、更易于测试和维护的Java应用程序。

热门AI工具

更多
DeepSeek
DeepSeek

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

豆包大模型
豆包大模型

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

通义千问
通义千问

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

腾讯元宝
腾讯元宝

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

文心一言
文心一言

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

讯飞写作
讯飞写作

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

即梦AI
即梦AI

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

ChatGPT
ChatGPT

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

相关专题

更多
c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

236

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

458

2024.03.01

数据库三范式
数据库三范式

数据库三范式是一种设计规范,用于规范化关系型数据库中的数据结构,它通过消除冗余数据、提高数据库性能和数据一致性,提供了一种有效的数据库设计方法。本专题提供数据库三范式相关的文章、下载和课程。

358

2023.06.29

如何删除数据库
如何删除数据库

删除数据库是指在MySQL中完全移除一个数据库及其所包含的所有数据和结构,作用包括:1、释放存储空间;2、确保数据的安全性;3、提高数据库的整体性能,加速查询和操作的执行速度。尽管删除数据库具有一些好处,但在执行任何删除操作之前,务必谨慎操作,并备份重要的数据。删除数据库将永久性地删除所有相关数据和结构,无法回滚。

2082

2023.08.14

vb怎么连接数据库
vb怎么连接数据库

在VB中,连接数据库通常使用ADO(ActiveX 数据对象)或 DAO(Data Access Objects)这两个技术来实现:1、引入ADO库;2、创建ADO连接对象;3、配置连接字符串;4、打开连接;5、执行SQL语句;6、处理查询结果;7、关闭连接即可。

349

2023.08.31

MySQL恢复数据库
MySQL恢复数据库

MySQL恢复数据库的方法有使用物理备份恢复、使用逻辑备份恢复、使用二进制日志恢复和使用数据库复制进行恢复等。本专题为大家提供MySQL数据库相关的文章、下载、课程内容,供大家免费下载体验。

256

2023.09.05

vb中怎么连接access数据库
vb中怎么连接access数据库

vb中连接access数据库的步骤包括引用必要的命名空间、创建连接字符串、创建连接对象、打开连接、执行SQL语句和关闭连接。本专题为大家提供连接access数据库相关的文章、下载、课程内容,供大家免费下载体验。

326

2023.10.09

数据库对象名无效怎么解决
数据库对象名无效怎么解决

数据库对象名无效解决办法:1、检查使用的对象名是否正确,确保没有拼写错误;2、检查数据库中是否已存在具有相同名称的对象,如果是,请更改对象名为一个不同的名称,然后重新创建;3、确保在连接数据库时使用了正确的用户名、密码和数据库名称;4、尝试重启数据库服务,然后再次尝试创建或使用对象;5、尝试更新驱动程序,然后再次尝试创建或使用对象。

412

2023.10.16

clawdbot ai使用教程 保姆级clawdbot部署安装手册
clawdbot ai使用教程 保姆级clawdbot部署安装手册

Clawdbot是一个“有灵魂”的AI助手,可以帮用户清空收件箱、发送电子邮件、管理日历、办理航班值机等等,并且可以接入用户常用的任何聊天APP,所有的操作均可通过WhatsApp、Telegram等平台完成,用户只需通过对话,就能操控设备自动执行各类任务。

14

2026.01.29

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 4.3万人学习

Pandas 教程
Pandas 教程

共15课时 | 1.0万人学习

ASP 教程
ASP 教程

共34课时 | 4.2万人学习

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

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