
本文介绍通过依赖注入与接口抽象解耦外部依赖,使构造函数中 catch 分支可被精准、稳定地测试,避免依赖不可控的系统行为(如 InetAddress.getLocalHost() 随机抛异常)。
本文介绍通过依赖注入与接口抽象解耦外部依赖,使构造函数中 catch 分支可被精准、稳定地测试,避免依赖不可控的系统行为(如 inetaddress.getlocalhost() 随机抛异常)。
在单元测试中,直接验证 catch 逻辑的最大难点在于:无法可靠触发目标异常。例如原代码中 InetAddress.getLocalHost().getHostName() 是否抛出 UnknownHostException 取决于运行时网络环境与主机配置,既不稳定也不可控——这违背了单元测试“可重复、可预测、快速执行”的基本原则。
解决方案是采用 依赖注入(Dependency Injection) + 接口抽象,将易变的底层调用封装为可替换的协作对象。具体步骤如下:
1. 提取可测试的抽象接口
定义一个职责单一的接口,封装主机名获取行为:
public interface HostNameSupplier {
String getHostName() throws UnknownHostException;
}
2. 提供默认实现(生产环境使用)
public class DefaultHostNameSupplier implements HostNameSupplier {
@Override
public String getHostName() throws UnknownHostException {
return InetAddress.getLocalHost().getHostName();
}
}
3. 改造被测类,支持依赖注入
修改 Hd 类,接受 HostNameSupplier 作为构造参数,移除硬编码调用:
public class Hd {
private final String hName;
public Hd(HostNameSupplier hostNameSupplier) {
String hostName;
try {
hostName = hostNameSupplier.getHostName();
} catch (UnknownHostException e) {
hostName = "unk-host";
}
this.hName = hostName;
}
// 假设提供 getter 用于断言(若原类未提供,请补充)
public String getHName() {
return hName;
}
}
✅ 优势:构造函数逻辑完全聚焦于业务流程(尝试获取 → 失败则降级),无副作用,且所有分支均可被确定性覆盖。
4. 编写精准的 JUnit 测试用例
在测试中注入自定义实现,强制触发异常路径:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Test
void shouldSetHNameToDefaultValueWhenUnknownHostExceptionThrown() {
// 给定:一个总是抛出 UnknownHostException 的模拟供应器
Hd hd = new Hd(new HostNameSupplier() {
@Override
public String getHostName() throws UnknownHostException {
throw new UnknownHostException("Simulated failure");
}
});
// 验证:构造后 hName 被正确设为降级值
assertEquals("unk-host", hd.getHName());
}
也可使用静态内部类提升可读性(如原答案所示),或结合 Mockito 等框架进一步简化(需添加 mockito-core 依赖):
@Test
void shouldSetHNameToDefaultValueUsingMockito() {
HostNameSupplier mockSupplier = mock(HostNameSupplier.class);
doThrow(new UnknownHostException()).when(mockSupplier).getHostName();
Hd hd = new Hd(mockSupplier);
assertEquals("unk-host", hd.getHName());
}
注意事项
- ❌ 避免在测试中试图“制造”真实网络异常(如禁用网卡、修改 hosts 文件),这会破坏测试隔离性与可移植性;
- ✅ 接口抽象粒度应合理:此处仅封装一行调用,符合单一职责,无过度设计;
- ? 若
Hd已有无参构造器且无法修改,可考虑添加包私有构造器(Hd(HostNameSupplier))供测试专用,同时保留原有构造器委托调用new Hd(new DefaultHostNameSupplier()); - ? 此模式同样适用于测试
IOException、SQLException等任何由外部资源引发的受检异常处理逻辑。
通过将不确定性依赖显式化、接口化,我们不仅让 catch 分支变得可测试,更提升了代码的可维护性与可扩展性——这是面向测试编程(Test-Driven Design)的核心实践之一。











