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 的实例:

SpeechEasy
SpeechEasy

SpeechEasy是一种合成语音解决方案,可以让用户从文本生成高质量、易于理解的音频。

下载
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 对象的行为。

最佳实践与注意事项

  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 并采用依赖注入模式,我们可以有效地解耦对象的创建过程。这种重构策略允许我们在单元测试中注入模拟的依赖对象,从而实现对被测单元行为的精确控制。尽管“模拟返回模拟”可能带来一些复杂性,但通过仔细设计和权衡,依赖注入是构建可测试、可维护和高弹性代码的重要实践。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

844

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

743

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

740

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

400

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

447

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

431

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16926

2023.08.03

c++空格相关教程合集
c++空格相关教程合集

本专题整合了c++空格相关教程,阅读专题下面的文章了解更多详细内容。

0

2026.01.23

热门下载

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

精品课程

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

共23课时 | 2.8万人学习

C# 教程
C# 教程

共94课时 | 7.4万人学习

Java 教程
Java 教程

共578课时 | 50.3万人学习

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

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