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

Mock的CloseableHttpClient仍发真实POST请求,仅改测试类如何解决?

问题:Spring Boot单元测试中无法Mock CloseableHttpClient导致发送真实请求

我编写了一个Spring Boot类的单元测试,该类通过POST请求向指定URL发送文件。但运行测试时,程序会向真实URL发送实际请求,推测是CloseableHttpClient未被正确Mock。

测试类代码

class FileSenderTest {
    @InjectMocks
    FileSender fileSender;
    
    @Mock
    UrlConfig urlConfig;
    
    @Mock
    InetAddress inetAddress;

    @Mock
    SSLContextFactory sslContextFactory = new SSLContextFactory();

    @BeforeEach
    void setUp() {
    }

    @AfterEach
    void tearDown() {
    }
   
    @Test
    public void postData() throws Exception {
        URL url = new URL("/someUrl");
        urlConfig.setURL(url);
        urlConfig.setPassword("somePassword123");
        fileSender = new FileSender(urlConfig);

        URL res = getClass().getClassLoader().getResource("test.json");
        File file = Paths.get(res.toURI()).toFile();
        String filePath = file.getAbsolutePath();
        
        HttpPost httpPost = mock(HttpPost.class);
        StatusLine statusLine = mock(StatusLine.class);
        CloseableHttpClient closeableHttpClient = mock(CloseableHttpClient.class);
        CloseableHttpResponse closeableHttpResponse = mock(CloseableHttpResponse.class);

        when(statusLine.getStatusCode()).thenReturn(200);
        when(closeableHttpResponse.getStatusLine()).thenReturn(statusLine);
        when(closeableHttpClient.execute(httpPost)).thenReturn(closeableHttpResponse);

        int response = fileSender.sendFile(filePath);

        Assertions.assertEquals(200, response);
    }

}

被测试类代码

@Service 
public class FileSender {
    private final UrlConfig urlConfig;
    SSLContext sslContext;
    
    @Autowired
    public FileSender(UrlConfig urlConfig) {
        this.urlConfig = urlConfig;
    }

    public int sendFile(String targetFileName) throws IOException {
        Path tracesPath = Paths.get(targetFileName);
        HttpPost httpPost = new HttpPost(this.urlConfig.getURL().toString());
        httpPost.addHeader("Accept", "application/json");

        MultipartEntityBuilder builder = MultipartEntityBuilder.create();        
        builder.addBinary("file", new ByteArrayInputStream(Files.readAllBytes(tracesPath)), ContentType.APPLICATION_OCTET_STREAM, targetFileName);

        HttpEntity multipart = builder.build();
        httpPost.setEntity(multipart);
        
        sslContext = this.urlConfig.getSSLContext();
        sslContext = SSLContextFactory.create();
        this.urlConfig.setSSLContext(sslContext);

       try (CloseableHttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build()) {
           CloseableHttpResponse response = httpClient.execute(httpPost);
           return response.getStatusLine().getStatusCode();
        }  catch (IOException e) {
           log.error(e.getMessage(), e);
        }
        return 500;
    }
}

问题:请问是否可以仅通过重构测试类来解决该问题?


回答

不行,仅靠重构测试类无法解决这个问题,核心原因是被测试类FileSender中直接在sendFile方法内硬编码创建了CloseableHttpClient实例:

CloseableHttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build()

这种局部创建的对象无法通过Mockito的@Mock或@InjectMocks替换,测试类里Mock的CloseableHttpClient根本不会被用到,程序依然会创建真实的客户端发送请求。

要解决这个问题,必须先修改FileSender类,把HttpClient的创建逻辑抽离出来,让它可以被测试Mock,然后再调整测试类。

步骤1:改造被测试类FileSender

推荐两种常见的改造方式:

方式1:抽取HttpClient创建方法,方便测试时覆盖

@Service 
public class FileSender {
    private final UrlConfig urlConfig;
    SSLContext sslContext;
    private static final Logger log = LoggerFactory.getLogger(FileSender.class);
    
    @Autowired
    public FileSender(UrlConfig urlConfig) {
        this.urlConfig = urlConfig;
    }

    public int sendFile(String targetFileName) throws IOException {
        Path tracesPath = Paths.get(targetFileName);
        HttpPost httpPost = new HttpPost(this.urlConfig.getURL().toString());
        httpPost.addHeader("Accept", "application/json");

        MultipartEntityBuilder builder = MultipartEntityBuilder.create();        
        builder.addBinary("file", new ByteArrayInputStream(Files.readAllBytes(tracesPath)), ContentType.APPLICATION_OCTET_STREAM, targetFileName);

        HttpEntity multipart = builder.build();
        httpPost.setEntity(multipart);
        
        sslContext = SSLContextFactory.create();
        this.urlConfig.setSSLContext(sslContext);

       try (CloseableHttpClient httpClient = createHttpClient(sslContext)) {
           CloseableHttpResponse response = httpClient.execute(httpPost);
           return response.getStatusLine().getStatusCode();
        }  catch (IOException e) {
           log.error(e.getMessage(), e);
        }
        return 500;
    }

    // 抽离成protected方法,方便测试时Mock
    protected CloseableHttpClient createHttpClient(SSLContext sslContext) {
        return HttpClients.custom().setSSLContext(sslContext).build();
    }
}

方式2:注入HttpClient工厂类(更符合依赖注入原则)

先定义工厂接口:

public interface HttpClientFactory {
    CloseableHttpClient create(SSLContext sslContext);
}

实现生产环境的工厂:

@Component
public class DefaultHttpClientFactory implements HttpClientFactory {
    @Override
    public CloseableHttpClient create(SSLContext sslContext) {
        return HttpClients.custom().setSSLContext(sslContext).build();
    }
}

修改FileSender注入工厂:

@Service 
public class FileSender {
    private final UrlConfig urlConfig;
    private final HttpClientFactory httpClientFactory;
    SSLContext sslContext;
    private static final Logger log = LoggerFactory.getLogger(FileSender.class);
    
    @Autowired
    public FileSender(UrlConfig urlConfig, HttpClientFactory httpClientFactory) {
        this.urlConfig = urlConfig;
        this.httpClientFactory = httpClientFactory;
    }

    public int sendFile(String targetFileName) throws IOException {
        // 省略其他代码...
        
       try (CloseableHttpClient httpClient = httpClientFactory.create(sslContext)) {
           CloseableHttpResponse response = httpClient.execute(httpPost);
           return response.getStatusLine().getStatusCode();
        }  catch (IOException e) {
           log.error(e.getMessage(), e);
        }
        return 500;
    }
}

步骤2:重构测试类

以方式1为例,测试类可以通过Spy来MockcreateHttpClient方法:

class FileSenderTest {
    @Mock
    UrlConfig urlConfig;

    @Mock
    SSLContextFactory sslContextFactory;

    @Mock
    CloseableHttpClient closeableHttpClient;
    
    @Mock
    CloseableHttpResponse closeableHttpResponse;
    
    @Mock
    StatusLine statusLine;

    // 使用Spy包装真实对象,允许覆盖特定方法
    @Spy
    FileSender fileSender;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
        // 初始化Spy对象
        fileSender = Mockito.spy(new FileSender(urlConfig));
    }

    @Test
    public void postData() throws Exception {
        // 配置Mock依赖行为
        URL url = new URL("http://fake-url.com");
        when(urlConfig.getURL()).thenReturn(url);
        SSLContext mockSslContext = mock(SSLContext.class);
        when(sslContextFactory.create()).thenReturn(mockSslContext);
        
        // Mock createHttpClient方法,返回自定义的Mock客户端
        doReturn(closeableHttpClient).when(fileSender).createHttpClient(mockSslContext);
        
        // Mock响应结果
        when(closeableHttpResponse.getStatusLine()).thenReturn(statusLine);
        when(statusLine.getStatusCode()).thenReturn(200);
        when(closeableHttpClient.execute(any(HttpPost.class))).thenReturn(closeableHttpResponse);

        // 准备测试文件
        URL res = getClass().getClassLoader().getResource("test.json");
        File file = Paths.get(res.toURI()).toFile();
        String filePath = file.getAbsolutePath();

        // 执行测试并断言结果
        int responseCode = fileSender.sendFile(filePath);
        Assertions.assertEquals(200, responseCode);
    }
}

如果用方式2,测试类直接MockHttpClientFactory即可:

class FileSenderTest {
    @Mock
    UrlConfig urlConfig;

    @Mock
    HttpClientFactory httpClientFactory;

    @Mock
    CloseableHttpClient closeableHttpClient;
    
    @Mock
    CloseableHttpResponse closeableHttpResponse;
    
    @Mock
    StatusLine statusLine;

    @InjectMocks
    FileSender fileSender;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void postData() throws Exception {
        URL url = new URL("http://fake-url.com");
        when(urlConfig.getURL()).thenReturn(url);
        SSLContext mockSslContext = mock(SSLContext.class);
        when(sslContextFactory.create()).thenReturn(mockSslContext);
        
        when(httpClientFactory.create(mockSslContext)).thenReturn(closeableHttpClient);
        when(closeableHttpResponse.getStatusLine()).thenReturn(statusLine);
        when(statusLine.getStatusCode()).thenReturn(200);
        when(closeableHttpClient.execute(any(HttpPost.class))).thenReturn(closeableHttpResponse);

        // 准备测试文件...
        
        int responseCode = fileSender.sendFile(filePath);
        Assertions.assertEquals(200, responseCode);
    }
}

总结

必须先修改FileSender类,将HttpClient的创建逻辑从方法内移到可被Mock的位置(要么抽成protected方法,要么注入工厂类),之后才能通过测试类Mock HttpClient,避免发送真实请求。仅靠测试类无法解决这个问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 01:25:20