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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 07:00:24