如何在单元测试中有效模拟方法返回的对象:解耦与依赖注入实践

如何在单元测试中有效模拟方法返回的对象:解耦与依赖注入实践

当被测类内部直接实例化依赖对象时,传统的模拟方法难以奏效。本文将探讨导致此问题的紧密耦合现象,并提供一种通过引入 `supplier` 接口进行依赖注入的重构策略。通过解耦对象的创建过程,我们能够有效地在单元测试中模拟依赖行为,从而提高代码的可测试性和维护性。

引言:理解测试中的模拟挑战

在单元测试中,我们经常需要模拟依赖对象的行为,以隔离被测单元并确保测试的专注性。然而,当被测类在内部直接创建其依赖对象的实例时,这种传统的模拟方法会遇到障碍。考虑以下 Java 代码示例:

class A {    public void foo() {        System.out.println("A's foo called");    }}class B {    public A foo() {        System.out.println("B's foo called");        return new A(); // B's foo returns a new A    }}class SomeClass {    public void doSomeThing() {        B b = new B(); // SomeClass internally creates B        A a = b.foo();        a.foo();    }}

假设我们希望测试 SomeClass 的 doSomeThing 方法,并模拟 B.foo() 返回的 A 对象。直观的尝试可能是使用 @Mock 注解来模拟 A,但这种方法通常会失败:

import org.junit.jupiter.api.Test;import org.mockito.InjectMocks;import org.mockito.Mock;import org.mockito.Mockito;import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;public class SomeClassTest {    @Mock    A aMock; // 尝试模拟 A    @InjectMocks    SomeClass someClass;    @Test    void testDoSomeThingFails() {        // 尝试配置 aMock 的行为,但这个 aMock 实例并不会被 SomeClass 使用        Mockito.when(aMock.foo()).thenReturn(/* 某些值或行为 */ null);         // 这里的测试会失败,因为 SomeClass 内部创建了 B 和 A 的实例        // 它并不知道我们创建的 aMock        assertDoesNotThrow(() -> someClass.doSomeThing());    }}

上述测试失败的原因在于 SomeClass 与 B 之间存在紧密的耦合。SomeClass 在 doSomeThing() 方法内部通过 new B() 直接创建了 B 的实例,进而调用 b.foo() 获取 A 的实例。测试框架无法拦截或替换这些在方法内部创建的具体实例,因此我们外部创建的 aMock 和 bMock 都不会被 SomeClass 所使用。

解决方案:通过依赖注入解耦

要解决这种紧密耦合带来的测试难题,核心思想是将对依赖对象的创建控制权从被测类内部转移到外部。这正是依赖注入(Dependency Injection, DI)模式所倡导的。通过允许外部在构造时或运行时提供依赖,我们可以轻松地在测试中注入模拟对象。

一种简洁有效的解耦策略是引入 java.util.function.Supplier 接口。Supplier 是一个函数式接口,它不接受任何参数并返回一个结果,非常适合用来“供应”或“提供”一个对象实例。

我们将重构 SomeClass,使其不再直接创建 B 的实例,而是通过一个 Supplier 来获取 B 的实例:

ImagetoCartoon ImagetoCartoon

一款在线AI漫画家,可以将人脸转换成卡通或动漫风格的图像。

ImagetoCartoon 106 查看详情 ImagetoCartoon

import java.util.function.Supplier;class SomeClass {  private final Supplier bFactory;  // 构造函数:允许外部注入如何创建 B 的逻辑  public SomeClass(final Supplier bFactory) {    this.bFactory = bFactory;  }  // 无参构造函数:为了向后兼容性或生产环境的便利  // 在生产代码中,它会使用默认的 B::new 来创建 B 的实例  public SomeClass() {    this(B::new);   }  public void doSomeThing() {    // 通过注入的 Supplier 获取 B 的实例    B b = this.bFactory.get();     A a = b.foo();    a.foo();  }}

在重构后的 SomeClass 中,B 对象的创建逻辑被抽象为 bFactory。在生产环境中,可以通过 new SomeClass(B::new) 来保持原有行为;而在测试中,我们可以注入一个返回模拟 B 对象的 Supplier。

测试重构后的代码

有了这种解耦,我们现在可以轻松地在单元测试中模拟 B 和 A 的行为:

import org.junit.jupiter.api.Test;import org.mockito.Mockito;import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;import static org.mockito.Mockito.mock;import static org.mockito.Mockito.when;public class SomeClassRefactoredTest {    @Test    void testDoSomeThingWithMocks() {        // 1. 创建 A 的模拟对象        final A aMock = mock(A.class);        // 2. 配置 A 模拟对象的行为 (如果需要)        // 例如:当 aMock.foo() 被调用时,不抛出异常        when(aMock.foo()).thenAnswer(invocation -> {            System.out.println("Mocked A's foo called");            return null; // 或者返回其他期望值        });        // 3. 创建 B 的模拟对象        final B bMock = mock(B.class);        // 4. 配置 B 模拟对象的 foo() 方法,使其返回 aMock        when(bMock.foo()).thenReturn(aMock);        // 5. 实例化 SomeClass,注入一个返回 bMock 的 Supplier        final SomeClass someClass = new SomeClass(() -> bMock);        // 6. 执行测试并断言        assertDoesNotThrow(() -> someClass.doSomeThing());        // 验证 mock 对象是否被正确调用 (可选)        Mockito.verify(bMock).foo();        Mockito.verify(aMock).foo();    }}

通过这种方式,我们成功地控制了 SomeClass 内部对 B 实例的获取过程,从而能够注入一个模拟的 B 对象,并进一步控制 B 返回的 A 对象的行为。

最佳实践与注意事项

避免“模拟返回模拟”(Mocks Returning Mocks):尽管上述解决方案有效,但值得注意的是,让一个模拟对象返回另一个模拟对象(即 bMock.foo() 返回 aMock)通常被认为是不良实践。这种设置会使测试变得脆弱、复杂,并与实现细节过度耦合。

脆弱性: 如果 B.foo() 的实际实现发生变化(例如,它开始返回 C 而不是 A 的子类),即使功能不变,测试也可能中断。复杂性: 增加了测试的理解难度,需要跟踪多个模拟对象的配置。耦合性: 测试不仅依赖于 SomeClass 的行为,还依赖于 B 和 A 之间的具体交互模式。

理想情况下,我们应该尽量模拟那些直接与被测单元交互的依赖。如果 A 是一个简单的数据对象(POJO),或者其行为不复杂,可以考虑返回一个真实的 A 实例,或者一个行为非常简单的 A 模拟。如果 A 自身具有复杂的行为且需要被模拟,那么可能需要重新评估 SomeClass、B 和 A 之间的职责划分。

设计可测试的代码:本教程的核心在于强调“设计可测试性”。依赖注入是实现这一目标的关键模式之一。通过将依赖对象的创建和管理外部化,我们不仅方便了测试,还降低了模块间的耦合度,提高了代码的灵活性和可维护性。在设计之初就考虑依赖注入,可以避免后期为了测试而进行大规模重构。

其他依赖注入方式:除了 Supplier 模式,还有其他实现依赖注入的方式,例如:

构造函数注入: 直接在构造函数中传入依赖对象实例(适用于依赖是具体实例而非创建逻辑)。Setter 注入: 通过公共的 setter 方法设置依赖对象。接口注入: 依赖对象实现特定接口,被测类通过该接口获取依赖。依赖注入框架: 使用 Spring、Guice 等框架自动化依赖的创建和注入过程,尤其适用于大型复杂应用。

总结

当被测类内部直接实例化其依赖对象时,传统的模拟方法会因紧密耦合而失效。通过引入 java.util.function.Supplier 并采用依赖注入模式,我们可以有效地解耦对象的创建过程。这种重构策略允许我们在单元测试中注入模拟的依赖对象,从而实现对被测单元行为的精确控制。尽管“模拟返回模拟”可能带来一些复杂性,但通过仔细设计和权衡,依赖注入是构建可测试、可维护和高弹性代码的重要实践。

以上就是如何在单元测试中有效模拟方法返回的对象:解耦与依赖注入实践的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1090764.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
谷歌浏览器怎么把一个标签页静音_Chrome标签页静音操作教程
上一篇 2025年12月2日 10:07:01
曝iPhone 16 Pro外观会更光滑:重回高亮中框
下一篇 2025年12月2日 10:07:07

相关推荐

  • 怎么清理C盘垃圾 C盘清理全攻略

    怎么清理C盘垃圾 C盘清理全攻略怎么清理C盘垃圾 C盘清理全攻略怎么清理C盘垃圾 C盘清理全攻略怎么清理C盘垃圾 C盘清理全攻略

    随着电脑使用时间的增长,windows系统的c盘常常会积累大量无用文件,造成系统运行缓慢、响应迟钝等问题。定期清理c盘不仅有助于释放宝贵的存储空间,还能显著提升电脑的整体性能。本文将为你提供几种实用的c盘清理方法,助你轻松优化系统。 一、利用系统自带的磁盘清理功能 Windows内置的磁盘清理工具是…

    2026年8月28日 用户投稿
    100
  • 夸克AI有哪些功能_夸克AI核心功能与应用场景全解析

    夸克AI有哪些功能_夸克AI核心功能与应用场景全解析夸克AI有哪些功能_夸克AI核心功能与应用场景全解析夸克AI有哪些功能_夸克AI核心功能与应用场景全解析夸克AI有哪些功能_夸克AI核心功能与应用场景全解析

    夸克AI通过五大核心模块实现多功能集成:AI超级框作为全场景任务中枢,支持自然语言指令生成文本、规划行程及处理长文档;深度思考基于通义大模型,具备逻辑推理与多轮对话能力,适用于复杂问题分析;AI相机结合多模态识别,实现拍照翻译、搜题与视觉交互;AI写作提供多文体内容生成与润色,适配社交、职场等场景;…

    2026年8月28日 用户投稿
    000
  • 545%! DeepSeek首披露成本利润率 专家:若在美国已是一家价值逾百亿美元公司

    中国ai新创公司deepseek近来「开源」一波波,上周六 (1日) 又有更大惊喜,全面揭秘deepseek-v3/r1推理系统,不仅公开其推理系统的核心优化方案,更首次披露成本获利率等关键数据,引发产业震动。 DeepSeek上周六在知乎平台发布首条文章,公布模型推理成本利润细节,并披露成本获利率…

    2026年8月28日
    100
  • 如何解决Laravel项目中短信通知的问题?使用Composer安装VonageNotificationChannel可以!

    可以通过一下地址学习composer:学习地址 在开发 laravel 项目时,短信通知功能是一个常见的需求,但也常常带来一系列问题。我最近在开发一个需要短信通知的应用时,遇到了配置复杂、发送失败率高以及维护困难等一系列挑战。这些问题不仅影响了用户体验,也让我在开发过程中感到头疼。 为了解决这些问题…

    用户投稿 2026年8月28日
    000
  • HTTP1.1与HTTP2的区别_HTTP1.1与HTTP2有哪些区别

    http/2通过多路复用在单个tcp连接上并行传输多个独立的数据流,每个流的数据被分割为带流id的二进制帧并交错发送,接收方根据流id重新组装,从而避免了http/1.1中因单个请求阻塞导致后续请求等待的队头阻塞问题;2. hpack头部压缩通过静态字典、动态字典和哈夫曼编码减少重复头部信息的传输量…

    2026年8月28日
    300
  • Spark HA集群搭建

    Spark HA集群搭建Spark HA集群搭建Spark HA集群搭建Spark HA集群搭建

    环境准备 我使用的是CentOS-6.6版本的4个虚拟机,主机名为hadoop01、hadoop02、hadoop03、hadoop04。集群将由hadoop用户搭建(在生产环境中,root用户通常不可随意使用)。关于虚拟机的安装,可以参考以下两篇文章:在Windows中安装一台Linux虚拟机,以…

    2026年8月28日 用户投稿
    000
  • 协程栈(Coroutine Stack)的内存管理

    协程栈的内存管理是通过用户态栈和运行时环境来实现的。1)在python中,协程使用生成器和yield机制,共享全局解释器锁,需处理暂停和恢复逻辑。2)在go中,goroutine使用m:n调度模型,运行时自动调整栈大小,防止栈溢出和内存泄漏。 在编程世界中,协程栈(Coroutine Stack)的…

    2026年8月28日
    000
  • PHP:高效检查多维数组中是否存在重复的嵌套项

    本文将探讨如何在 php 中有效检查一个多维数组是否已包含具有相同嵌套项的元素。针对 `in_array()` 函数在处理复杂数组时的局限性,我们将介绍通过迭代和直接比较嵌套子数组的方法,确保在添加新数据前避免重复,从而维护数据结构的唯一性和完整性。 在 PHP 开发中,我们经常需要处理包含复杂数据…

    2026年8月28日
    100
  • 电脑提示缺少“api-ms-win-core-path-l1-1-0.dll”的4种解决方案

    电脑提示缺少“api-ms-win-core-path-l1-1-0.dll”的4种解决方案电脑提示缺少“api-ms-win-core-path-l1-1-0.dll”的4种解决方案电脑提示缺少“api-ms-win-core-path-l1-1-0.dll”的4种解决方案电脑提示缺少“api-ms-win-core-path-l1-1-0.dll”的4种解决方案

    当你在电脑上启动某些软件或游戏时,突然弹出提示:“缺少 api-ms-win-core-path-l1-1-0.dll 文件,程序无法启动”,这确实令人困扰。这通常是由于系统所需的运行库文件丢失或损坏所引起的。别担心,遇到此类问题无需慌张。本文将为你提供4种实用的解决方案,帮你快速修复缺失的 dll…

    2026年8月28日 用户投稿
    000
  • 耶鲁、剑桥等开发MindLLM,将脑成像直接转换为文本

    耶鲁、剑桥等开发MindLLM,将脑成像直接转换为文本耶鲁、剑桥等开发MindLLM,将脑成像直接转换为文本耶鲁、剑桥等开发MindLLM,将脑成像直接转换为文本耶鲁、剑桥等开发MindLLM,将脑成像直接转换为文本

    ☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜ 编辑 | 萝卜皮 将功能性磁共振成像 (fMRI) 信号解码为文本一直是神经科学界面临的一项重大挑战,它有望推动脑机接口的发展,并加深对大脑机制的了解。然而,现有的方法往往存在预测性能不佳、任务…

    2026年8月28日 用户投稿
    000
  • 首个强化生成模型AbNovo实现多目标、多约束抗体从头设计,发表于ICLR 2025

    首个强化生成模型AbNovo实现多目标、多约束抗体从头设计,发表于ICLR 2025首个强化生成模型AbNovo实现多目标、多约束抗体从头设计,发表于ICLR 2025首个强化生成模型AbNovo实现多目标、多约束抗体从头设计,发表于ICLR 2025首个强化生成模型AbNovo实现多目标、多约束抗体从头设计,发表于ICLR 2025

    上海交大医学院张海仓课题组联合中原人工智能产业技术研究院及中科院计算所,研发出一款名为abnovo的创新抗体设计工具。该工具巧妙地结合强化学习和深度扩散模型,能够在多重目标和约束条件下,高效完成抗体从头设计。这项研究成果已发表于国际顶级机器学习会议iclr 2025,论文标题为“multi-obje…

    2026年8月28日 用户投稿
    000
  • 如何在Yii中实现软删除(Soft Delete)?

    在yii框架中实现软删除可以通过在模型中添加is_deleted字段,并重写beforedelete和find方法来实现。1. 在模型中添加is_deleted字段。2. 重写beforedelete方法,将is_deleted设为true并保存。3. 修改find方法,默认只返回未删除的记录。软删…

    2026年8月28日
    100
  • laravel表单请求(Form Request)如何使用_Laravel表单请求Form Request使用方法

    Laravel的Form Request用于分离验证与授权逻辑。1. 使用artisan命令生成请求类;2. 在authorize中设置权限,rules中定义规则;3. 可自定义messages错误信息;4. 控制器直接注入使用,自动验证;5. 可通过prepareForValidation预处理数…

    2026年8月28日
    000
  • 《要塞十字军东征:决定版》现已登陆Steam 获特别好评

    《要塞十字军东征:决定版》现已登陆Steam 获特别好评《要塞十字军东征:决定版》现已登陆Steam 获特别好评《要塞十字军东征:决定版》现已登陆Steam 获特别好评《要塞十字军东征:决定版》现已登陆Steam 获特别好评

    经典城堡模拟rts作品《要塞十字军东征:决定版(stronghold crusader: definitive edition)》已于昨日正式登陆pc steam平台,目前正处于首发优惠期间。游戏原价76元,享受10%折扣后现价68.4元,支持简体中文语言,并已收获玩家“特别好评”。 准备好你的武器…

    2026年8月28日 用户投稿
    500
  • 日志切割(Log Rotation)与集中管理

    我们需要日志切割和集中管理,因为它们能有效管理和分析系统日志。1) 日志切割将大文件分割成小文件,解决磁盘空间问题并简化日志查找和分析。2) 集中管理将不同来源的日志汇总到一起,方便统一监控和分析,提升系统运维效率。 日志切割和集中管理是运维中非常重要的两个概念,它们帮助我们更好地管理和分析系统日志…

    2026年8月28日
    000
  • 跨微服务数据库查询效率低怎么办?

    优化跨微服务数据库查询效率的策略 本文探讨一个Java微服务架构下,跨数据库查询效率低下的问题。微服务A和B分别连接不同的数据库,且不在同一台服务器上。微服务B需要获取微服务A中未导入到自身数据库的人员信息。当前方案是:先从微服务B数据库获取已导入人员ID列表,然后在微服务A使用select * f…

    2026年8月28日
    000
  • 使用Workerman实现WebSocket服务

    使用workerman实现websocket服务的步骤包括:1) 使用简洁的api快速搭建websocket服务器,只需几行代码;2) 处理连接建立、消息接收和连接关闭的回调函数;3) 优化连接管理、消息处理和错误处理;4) 注意资源管理和垃圾回收,以提升性能。 你想知道如何使用Workerman实…

    2026年8月28日
    000
  • 小心二手Swtich2 买回来可能变砖

    除了硬件质量问题之外,想要入手二手任天堂switch 2的玩家们还需留意因网络服务封锁而导致主机无法正常使用的情况。尽管这些限制措施是任天堂为了打击盗版和防止主机被破解所采取的手段,但也引发了关于switch 2长期使用性的担忧。 近日一位Reddit用户分享了自己的经历,他在沃尔玛以低于市场价50…

    2026年8月28日
    000
  • 小米手机Android 16更新名单/日期曝光 覆盖多款旗舰

    小米继谷歌之后,成为首家向用户推送android 16稳定版更新的手机厂商。该更新基于hyperos 2.3系统,小米15系列已在全球范围内率先收到此次升级。据cnmo了解,海外媒体近日曝光了小米设备适配android 16的计划。 虽然小米尚未正式发布完整的支持机型列表,但从以往更新策略来看,预计…

    2026年8月28日
    000
  • Laravel邮件服务(Mail Services)集成

    在laravel中集成邮件服务可以通过以下步骤实现:1) 配置邮件驱动,如smtp、mailgun或sendmail;2) 使用mail facade发送邮件,支持纯文本和html格式;3) 创建并使用邮件模板提高可维护性;4) 利用队列功能提高邮件发送的可靠性和效率;5) 设置速率限制和监控日志以…

    2026年8月28日
    100

发表回复

登录后才能评论
关注微信