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
相关产品推荐
相关产品推荐

