
本文介绍在无法直接注入依赖的场景下,通过重构构造逻辑(如添加可测试构造函数)或合理使用 Mockito 的 mockedConstruction 等方案,安全、可靠地模拟私有 final 客户端对象的行为,避免真实外部调用并支持异常断言。
本文介绍在无法直接注入依赖的场景下,通过重构构造逻辑(如添加可测试构造函数)或合理使用 mockito 的 `mockedconstruction` 等方案,安全、可靠地模拟私有 final 客户端对象的行为,避免真实外部调用并支持异常断言。
在单元测试中,当目标类(如 class A)内部直接 new 出一个私有 final 依赖(如 B b = new B(x, y)),且该依赖的方法(如 externalMethod())触发了不可控的外部行为(网络请求、数据库操作等)或需验证特定异常路径时,传统 mock 注入方式失效——因为 b 不是通过参数传入,无法在测试中替换。
✅ 推荐方案:面向测试的设计重构(首选)
最健壮、可维护性最强的方式是解耦对象创建与使用逻辑。为 A 类增加一个接受 B 实例的构造函数(可设为 package-private 或 public),使其支持依赖注入:
class A {
private final B b;
// 原有构造函数(供生产代码使用)
public A(String x, String y) {
this(new B(x, y)); // 委托给新构造函数
}
// 新增可测试构造函数(推荐 package-private,避免暴露给外部模块)
A(B b) {
this.b = Objects.requireNonNull(b);
}
public void someMethod() {
b.externalMethod();
}
}
测试时即可精准控制依赖行为:
@Test
void testSomeMethod_ThrowsException() {
B mockB = Mockito.mock(B.class);
doThrow(new RuntimeException("simulated failure"))
.when(mockB).externalMethod();
A a = new A(mockB); // 直接注入 mock
assertThatThrownBy(a::someMethod)
.isInstanceOf(RuntimeException.class)
.hasMessage("simulated failure");
}
✅ 优势:线程安全、支持任意次数调用、可完整验证交互(verify)、异常断言自然清晰;❌ 缺点:需少量代码调整(但属于良好设计实践)。
⚠️ 备选方案:谨慎使用 mockedConstruction(仅限无法重构时)
若因历史约束暂时无法修改 A 的构造逻辑,可借助 Mockito 4.0+ 的 mockedConstruction,但需规避其固有限制:
- 问题1(单线程单 mock 冲突):每个测试方法应独立调用 mockedConstruction,避免跨测试复用;
- 问题2(异常无法延迟触发):不能在 lambda 中直接 doThrow(...).when(...),而应在 mock 创建后、实际调用前配置:
@Test
void testWithMockedConstruction() {
try (MockedConstruction<b> mocked = Mockito.mockedConstruction(B.class)) {
// 获取首次创建的 mock 实例(注意:mocked.constructed().get(0))
B createdB = mocked.constructed().get(0);
doThrow(new IOException("network timeout"))
.when(createdB).externalMethod();
A a = new A("test-x", "test-y");
assertThatThrownBy(a::someMethod)
.isInstanceOf(IOException.class);
}
}</b>
⚠️ 注意事项:
- mockedConstruction 作用于构造过程本身,对已存在的实例无效;
- 必须确保 B 的构造无副作用(否则会影响测试稳定性);
- 不适用于 B 被多次构造的场景(如循环中 new 多个 B),此时应优先重构。
? 总结建议
| 场景 | 推荐做法 |
|---|---|
| 可修改被测类 | 添加依赖注入构造函数 —— 最简洁、可读性强、符合 SOLID 原则 |
| 严格禁止修改源码 | 使用 mockedConstruction + 显式 mock 配置,但需覆盖所有构造路径并注意线程隔离 |
| B 行为可通过输入参数触发异常 | 优先采用真实对象 + 可控输入(如传入非法 x/y),实现零 mock 测试 |
最终,测试友好性往往源于设计合理性。与其在测试中“对抗”难以模拟的代码,不如将依赖显式化——这不仅解决当前问题,更提升系统可维护性与可演进性。











