0

0

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

心靈之曲

心靈之曲

发布时间:2025-11-13 10:42:16

|

144人浏览过

|

来源于php中文网

原创

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

当被测类内部直接实例化依赖对象时,传统的模拟方法难以奏效。本文将探讨导致此问题的紧密耦合现象,并提供一种通过引入 `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 的实例:

飞书多维表格
飞书多维表格

表格形态的AI工作流搭建工具,支持批量化的AI创作与分析任务,接入DeepSeek R1满血版

下载
import java.util.function.Supplier;

class SomeClass {
  private final Supplier<? extends B> bFactory;

  // 构造函数:允许外部注入如何创建 B 的逻辑
  public SomeClass(final Supplier<? extends B> 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 对象的行为。

最佳实践与注意事项

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

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

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

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

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

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

总结

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

热门AI工具

更多
DeepSeek
DeepSeek

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

豆包大模型
豆包大模型

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

WorkBuddy
WorkBuddy

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

腾讯元宝
腾讯元宝

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

文心一言
文心一言

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

讯飞写作
讯飞写作

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

即梦AI
即梦AI

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

ChatGPT
ChatGPT

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

相关专题

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

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

156

2025.08.06

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

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

88

2026.01.26

spring框架介绍
spring框架介绍

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

156

2025.08.06

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

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

88

2026.01.26

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1925

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

656

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2395

2025.12.29

java接口相关教程
java接口相关教程

本专题整合了java接口相关内容,阅读专题下面的文章了解更多详细内容。

47

2026.01.19

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

热门下载

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

精品课程

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

共23课时 | 4.3万人学习

C# 教程
C# 教程

共94课时 | 11.1万人学习

Java 教程
Java 教程

共578课时 | 80.6万人学习

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

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