
当被测类内部直接实例化依赖对象时,传统的模拟方法难以奏效。本文将探讨导致此问题的紧密耦合现象,并提供一种通过引入 `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
一款在线AI漫画家,可以将人脸转换成卡通或动漫风格的图像。
106 查看详情
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
微信扫一扫
支付宝扫一扫