本文讲解当被测类在方法内部直接 new 出依赖对象(如 MyHttpClient)时,如何通过重构+接口抽象实现可测试性,避免无法 mock 的困境,并提供两种专业级解决方案。
本文讲解当被测类在方法内部直接 new 出依赖对象(如 `myhttpclient`)时,如何通过重构+接口抽象实现可测试性,避免无法 mock 的困境,并提供两种专业级解决方案。
在使用 Mockito 进行单元测试时,一个常见却棘手的问题是:被测方法内部通过 new 关键字创建了协作对象(如 MyHttpClient),导致该对象的实例方法(如 performCall())无法被 Mockito 拦截和模拟。你当前的 MyDataSourceImpl.execute() 方法正是如此——它在运行时动态构造 MyHttpClient,而 Mockito 默认只能 mock 由 Spring 容器管理或显式注入的依赖,对 new 出来的对象无能为力。
你尝试添加 @Mock private MyHttpClient myClient; 并设置 when(myClient.performCall()).thenReturn(...) 是无效的,因为测试中实际执行的是 new MyHttpClient(...).performCall(),与你声明的 mock 实例完全无关——它们是两个不同的对象。
✅ 推荐方案一:依赖注入重构(首选,符合测试友好设计)
最根本、最可持续的解法是将硬编码的 new 调用升级为依赖注入,让 MyHttpClient 成为 MyDataSourceImpl 的可注入协作方:
// 1. 修改被测类:注入 HttpClient(而非在方法内 new)
public class MyDataSourceImpl implements MyDataSource {
@Autowired
private PropertieService propertieService;
// 新增:注入客户端(可通过构造器注入更佳,此处保留 @Autowired 示例)
@Autowired
private MyHttpClient myClient; // ← 不再在 execute() 中 new!
public HttpResponse execute(String url) {
// 直接调用已注入的 client,便于 mock
HttpResponse response = myClient.performCall(url, propertieService.getProperties());
this.someOtherStuff(response);
return response;
}
}
对应测试只需注入 mock 客户端即可:
@RunWith(MockitoJUnitRunner.class)
public class MyDataSourceImplTest {
@InjectMocks
private MyDataSourceImpl myDataSourceImpl;
@Mock
private PropertieService propertieService;
@Mock
private MyHttpClient myClient; // ← 现在有效!Mockito 会将其注入到 myDataSourceImpl
@Test
public void simpleCall() {
// 给 stub 依赖
when(propertieService.getProperties()).thenReturn(new HashMap());
HttpResponse mockResponse = new HttpResponse(); // 假设构造合法
when(myClient.performCall("https://nothing.com", anyMap())).thenReturn(mockResponse);
// 执行 & 验证
HttpResponse response = myDataSourceImpl.execute("https://nothing.com");
assertEquals(mockResponse, response);
}
}
✅ 优势:代码更松耦合、职责清晰、天然可测;✅ 符合 Spring 最佳实践;✅ 无需额外适配层。
✅ 推荐方案二:引入工厂/策略接口(适用于无法修改 MyHttpClient 构造逻辑的场景)
若因历史约束无法将 MyHttpClient 改为 Spring Bean(例如它依赖非 Spring 管理的资源),则应引入抽象层隔离对象创建:
// 1. 定义可 mock 的行为契约
public interface HttpClientCaller {
HttpResponse performCall(String url, Map<string string> props);
}
// 2. 提供默认实现(适配器)
@Component
public class DefaultHttpClientCaller implements HttpClientCaller {
private final PropertieService propertieService;
public DefaultHttpClientCaller(PropertieService propertieService) {
this.propertieService = propertieService;
}
@Override
public HttpResponse performCall(String url, Map<string string> props) {
return new MyHttpClient(url, props).performCall(); // ← 封装 new 逻辑
}
}
// 3. 修改被测类:依赖接口而非具体类
public class MyDataSourceImpl implements MyDataSource {
@Autowired
private PropertieService propertieService;
@Autowired
private HttpClientCaller httpClientCaller; // ← 注入接口
public HttpResponse execute(String url) {
HttpResponse response = httpClientCaller.performCall(
url,
propertieService.getProperties()
);
this.someOtherStuff(response);
return response;
}
}</string></string>
测试时 mock 接口即可:
@Test
public void simpleCallWithAdapter() {
when(propertieService.getProperties()).thenReturn(new HashMap());
HttpResponse mockResponse = new HttpResponse();
when(httpClientCaller.performCall("https://nothing.com", anyMap()))
.thenReturn(mockResponse);
HttpResponse result = myDataSourceImpl.execute("https://nothing.com");
assertEquals(mockResponse, result);
}
⚠️ 注意:避免使用 PowerMock 等字节码增强工具 mock 构造函数——它增加测试脆弱性、降低可维护性,且违背“测试驱动设计”倡导的可测性即设计质量指标。
总结
- 不要在业务方法中 new 关键协作对象:这是可测试性的最大障碍;
- 优先采用依赖注入重构:将 new 上提至容器或构造器,让 Mockito 自然生效;
- 次选接口抽象 + 适配器模式:用一层轻量接口解耦创建逻辑,保持测试可控性;
- 所有方案的核心思想一致:将“创建”与“使用”分离,使“使用”部分可被替换(mock)。
遵循以上原则,你的 MyDataSourceImpl 不仅能被可靠测试,其设计本身也将更健壮、更易演进。











