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

