JUnit 5无法Mock Java.net.http.HttpClient.send问题排查求助
问题排查:JUnit 5 Mock HttpClient未生效,真实API仍被调用
在Java Maven非SpringBoot项目中编写JUnit 5测试代码时,尝试Mock API调用,但执行测试时仍会调用真实API,不符合预期。以下是业务代码和测试代码:
实际业务代码
public class MyService { public void create(String apiHost, String firstPage) { HttpClient httpClient = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_2) .connectTimeout(Duration.ofSeconds(30)) .build(); HttpRequest httpRequest = getHttpRequest(apiHost, firstPage); MyResponse myResponse = invokeApi(httpClient, httpRequest); List<ResponseData> responseDataList = new ArrayList<>(myResponse.getData()); while (null != myResponse.getLinks().getNext()) { myResponse = invokeApi(httpClient, getHttpRequest(apiHost, myResponse.getLinks().getNext())); responseDataList.addAll(myResponse.getData()); } createFile(fileName, responseDataList); } private HttpRequest getHttpRequest(String apiHost, String pageUri) { return HttpRequest.newBuilder() .header("Content-Type", "application/json") .GET() .uri(URI.create(apiHost + pageUri)) .build(); } @SneakyThrows MyResponse invokeApi(HttpClient httpClient, HttpRequest httpRequest) { HttpResponse<Supplier<MyResponse>> result = httpClient.send(httpRequest, new JsonBodyHandler<>(MyResponse.class)); return result.body().get(); } @SneakyThrows void createFile(String fileName, List<ResponseData> responseDataList) { Path filePath = Paths.get("test/path" + fileName); Files.createFile(filePath); Files.writeString(filePath, header + System.lineSeparator(), StandardOpenOption.APPEND); for (ResponseData responseData : responseDataList) { Files.writeString(filePath, responseData + System.lineSeparator(), StandardOpenOption.APPEND); } } }
JUnit 5测试代码
@ExtendWith(MockitoExtension.class) class MyServiceTests { @InjectMocks private MyService myService; @Mock private HttpClient httpClient; @Mock HttpResponse<Object> httpResponse; @Mock private HttpRequest request; @Mock Supplier<MyResponse> myResponseSupplier; @Test void testSendMessage() throws Exception { when(httpClient.send(any(),any())).thenReturn(httpResponse); when(httpResponse.body()).thenReturn(myResponseSupplier); when(myResponseSupplier.get()).thenReturn(getMyResponse()); myService.create("https://www.google.com", "/v1/test?page_size=100"); verify(httpClient).send(request, HttpResponse.BodyHandlers.ofString()); } }
问题原因及修复方案
核心问题
业务代码的create方法中直接在内部创建了HttpClient实例(HttpClient httpClient = HttpClient.newBuilder().build()),导致测试类中@Mock的HttpClient和业务代码使用的HttpClient完全是两个独立对象,Mockito无法对局部创建的对象进行Mock,因此测试时会走真实API调用。
修复步骤
1. 重构业务代码,将HttpClient改为依赖注入
修改MyService,通过构造方法注入HttpClient,同时保留无参构造用于生产环境:
public class MyService { private final HttpClient httpClient; // 构造注入,用于测试时传入Mock实例 public MyService(HttpClient httpClient) { this.httpClient = httpClient; } // 无参构造,生产环境默认创建HttpClient public MyService() { this.httpClient = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_2) .connectTimeout(Duration.ofSeconds(30)) .build(); } public void create(String apiHost, String firstPage) { // 不再创建局部HttpClient,直接使用成员变量 HttpRequest httpRequest = getHttpRequest(apiHost, firstPage); // 后续业务逻辑保持不变... } // 其他方法(getHttpRequest、invokeApi、createFile)保持不变 }
2. 调整测试代码,修正Mock和验证逻辑
- 修正HttpResponse的泛型,匹配业务代码中使用的
HttpResponse<Supplier<MyResponse>> - 修正verify的参数匹配,业务代码中使用的是自定义
JsonBodyHandler,不能用HttpResponse.BodyHandlers.ofString() - 处理分页场景的多次调用返回:
@ExtendWith(MockitoExtension.class) class MyServiceTests { @InjectMocks private MyService myService; @Mock private HttpClient httpClient; @Mock private HttpResponse<Supplier<MyResponse>> httpResponse; @Mock private Supplier<MyResponse> myResponseSupplier; @Test void testCreate() throws Exception { // 构造第一页响应(带下一页链接) MyResponse firstPageResponse = new MyResponse(); firstPageResponse.setData(Arrays.asList(new ResponseData())); Links firstPageLinks = new Links(); firstPageLinks.setNext("/v1/test?page_size=100&page=2"); firstPageResponse.setLinks(firstPageLinks); // 构造第二页响应(无下一页链接) MyResponse secondPageResponse = new MyResponse(); secondPageResponse.setData(Arrays.asList(new ResponseData())); Links secondPageLinks = new Links(); secondPageLinks.setNext(null); secondPageResponse.setLinks(secondPageLinks); // Mock HttpClient.send方法,返回预设响应 when(httpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) .thenReturn(httpResponse); when(httpResponse.body()).thenReturn(myResponseSupplier); // 模拟两次调用返回不同结果(对应分页) when(myResponseSupplier.get()) .thenReturn(firstPageResponse) .thenReturn(secondPageResponse); // 执行测试方法 myService.create("https://www.google.com", "/v1/test?page_size=100"); // 验证HttpClient.send被调用两次(两次分页请求) verify(httpClient, times(2)).send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)); } // 辅助方法:构造测试用MyResponse(按需填充数据) private MyResponse getMyResponse() { return new MyResponse(); } }
3. 额外注意事项
- 业务代码中
invokeApi使用了自定义JsonBodyHandler,测试时HttpResponse的泛型必须和业务代码一致,否则会因类型不匹配导致Mock失效。 - 测试中的
verify不能用预先Mock的request对象,因为业务代码中的HttpRequest是动态生成的,需用any(HttpRequest.class)这类匹配器来匹配。 - 若无法修改业务代码的构造方式,可通过反射替换局部HttpClient,但这种方式会增加测试复杂度,不推荐。
内容的提问来源于stack exchange,提问作者springenthusiast
相关产品推荐
相关产品推荐

