You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TDD、单元测试与Mock注入:如何兼顾单一职责原则?

问题核心拆解

你遇到的矛盾点很典型:自己封装的CustomHttpClient硬编码创建HttpClient实例,导致单元测试没法用Mockito mock依赖;想做依赖注入又觉得让使用者操心内部依赖不优雅,还怕违背单一职责原则。

正确的设计方案及原因

1. 双构造函数方案(最推荐)

给CustomHttpClient提供两个构造函数:一个无参的对外默认构造,内部自动创建标准HttpClient;一个带HttpClient参数的构造,专门给测试或特殊场景用。

代码示例:

import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.Objects;
import com.google.common.annotations.VisibleForTesting;

public class CustomHttpClient {
    private final HttpClient httpClient;

    // 对外公开的默认构造,使用者直接调用就行,完全不用管HttpClient的创建
    public CustomHttpClient() {
        this(HttpClient.newBuilder().build());
    }

    // 测试专用构造,用@VisibleForTesting标注明确用途,避免被误用
    @VisibleForTesting
    public CustomHttpClient(HttpClient httpClient) {
        this.httpClient = Objects.requireNonNull(httpClient, "HttpClient不能为null");
    }

    // 你的业务请求方法示例
    public HttpResponse<String> sendGetRequest(String url) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create(url))
                .GET()
                .build();
        return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
    }
}

为什么这个方案靠谱?

  • 对普通使用者完全透明:他们只用无参构造,根本不知道内部有依赖注入这回事,封装性拉满;
  • 测试时直接用带参构造注入Mock:比如用Mockito mock一个HttpClient,预设响应结果,完美满足TDD需求;
  • 完全不违背单一职责:CustomHttpClient的核心职责还是封装业务HTTP请求逻辑,HttpClient只是它完成职责的工具,提供构造参数只是给类加了个扩展点,没让它承担额外职责。

2. 静态工厂+测试专用替换方法(适合强封装场景)

如果不想暴露带参构造,可以用静态工厂方法创建实例,同时加一个仅测试用的静态方法替换默认的HttpClient。

代码示例:

import java.net.http.HttpClient;
import com.google.common.annotations.VisibleForTesting;

public class CustomHttpClient {
    private static HttpClient defaultHttpClient = HttpClient.newBuilder().build();
    private final HttpClient httpClient;

    // 私有构造,完全封装实例创建逻辑
    private CustomHttpClient(HttpClient httpClient) {
        this.httpClient = Objects.requireNonNull(httpClient);
    }

    // 对外公开的工厂方法,使用者唯一的创建入口
    public static CustomHttpClient create() {
        return new CustomHttpClient(defaultHttpClient);
    }

    // 测试专用的替换方法,只能在测试里调用
    @VisibleForTesting
    public static void setDefaultHttpClient(HttpClient mockClient) {
        defaultHttpClient = Objects.requireNonNull(mockClient);
    }

    // 业务方法...
}

这个方案的好处是构造函数完全私有,外部无法随意创建实例,测试时通过静态方法替换默认依赖,使用者完全感知不到内部细节。

3. 关于“使用者要注入依赖”的顾虑

你担心的“让使用者承担注入内部依赖的责任”其实是个伪命题——上面的方案里,普通使用者根本不需要主动注入,默认构造/工厂方法已经帮他们处理好了所有内部依赖。只有测试或者需要自定义HttpClient(比如配置超时、代理)的特殊场景,才会用到带参构造或替换方法。

单一职责原则的核心是“一个类只负责一项职责”,CustomHttpClient的职责是封装业务HTTP请求,而不是创建HttpClient——把创建HttpClient的逻辑从业务方法里抽离,本身就是符合单一职责的(业务逻辑和依赖创建解耦)。

为测试设计是否可接受?

当然可接受,而且是优秀代码的必备属性。可测试性本身就是衡量代码质量的重要标准,TDD的核心就是通过测试驱动出更灵活、更健壮的代码。为了可测试性做的合理设计,不是“为了测试妥协”,而是“提前为代码的可维护性、扩展性铺路”。

内容的提问来源于stack exchange,提问作者javacomelava

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 21:32:24