使用Mockito模拟Apache Post HTTPClient时触发UnknownHostException报错
问题原因
你的Mock完全没有生效,实际执行时走的是真实的HTTP请求,所以触发了未知域名报错,核心问题有3个:
- 业务代码里
CloseableHttpClient client = HttpClients.createDefault()是在方法内部直接新建的真实实例,你测试类里声明的@Mock CloseableHttpClient closeableHttpClient从来没有被传入到业务类中,和实际执行方法用到的client完全是两个对象。 - 你在测试中写的stub
given(closeableHttpClient.execute(httpPost)).willReturn(closeableHttpResponse)里的httpPost也是你自己Mock的实例,和业务方法内部new HttpPost(httpUrl)生成的真实对象不是同一个,参数匹配不上,就算client被注入了这个stub也不会触发。 - 测试方法漏加
@Test注解,且你调用ProductService.getAccessToken()是静态类调用,和你初始化的productService实例没有关联。
修复方案
优先选重构业务代码的方案,不要在方法内部硬编码初始化依赖,否则单元测试很难隔离外部依赖。
方案1:重构依赖注入(推荐)
把HttpClient从方法内部的局部变量改成类的成员变量,通过构造器传入,生产环境传入HttpClients.createDefault()的真实实例,测试时传入Mock实例即可。
修改后的业务代码:
public class ProductService { private final VaultConfig vaultConfig; private final AppConfig appConfig; private final CloseableHttpClient httpClient; // 构造器注入HttpClient,解耦初始化逻辑 public ProductService(VaultConfig vaultConfig, AppConfig appConfig, CloseableHttpClient httpClient) { this.vaultConfig = vaultConfig; this.appConfig = appConfig; this.httpClient = httpClient; } private void createAccessToken() { String httpUrl = MessageFormat.format("{0}/api/v1/authentication/login", appConfig.getProductServerUrl()); // 删除方法内部直接创建HttpClient的逻辑 HttpPost httpPost = new HttpPost(httpUrl); List<NameValuePair> httpParams = new ArrayList<NameValuePair>(); httpParams.add(new BasicNameValuePair("username", this.vaultConfig.getProductAdminUsername())); httpParams.add(new BasicNameValuePair("password", this.vaultConfig.getProductAdminPassword())); try { httpPost.setEntity(new UrlEncodedFormEntity(httpParams)); CloseableHttpResponse closeableHttpResponse = httpClient.execute(httpPost); HttpEntity entity = closeableHttpResponse.getEntity(); String tokenDataJson = EntityUtils.toString(entity, "UTF-8"); String newAccessToken = new Gson().fromJson(tokenDataJson, Map.class).get("access_token").toString(); this.vaultConfig.setProductAccessToken(newAccessToken); } catch (Exception e) { logger.error("Unable to create access token: " + e.getMessage()); } } }
对应修改后的测试代码:
public class ProductServiceTest { private ProductService productService; @Mock CloseableHttpClient closeableHttpClient; @Mock CloseableHttpResponse closeableHttpResponse; @Mock private VaultConfig vaultConfig; @Mock private AppConfig appConfig; @Mock private HttpEntity httpEntity; @BeforeEach public void initialize() throws IOException { MockitoAnnotations.openMocks(this); // 注入Mock的HttpClient到业务实例 productService = new ProductService(vaultConfig, appConfig, closeableHttpClient); // 配置通用Stub,用any()匹配方法内部生成的真实对象,不要用自己Mock的HttpPost实例匹配 given(appConfig.getProductServerUrl()).willReturn("https://test.abcd.com"); given(closeableHttpClient.execute(any(HttpPost.class))).willReturn(closeableHttpResponse); given(closeableHttpResponse.getEntity()).willReturn(httpEntity); given(EntityUtils.toString(any(HttpEntity.class), eq("UTF-8"))).willReturn("{\"access_token\":\"ABC\"}"); } @Test void getAccessTokenWhenEmpty() throws IOException { //given String expectedProductAccessToken = "ABC"; given(vaultConfig.getProductAccessToken()).willReturn(""); //when // 用初始化好的业务实例调用方法,不要静态调用 String actualProductAccessToken = productService.getAccessToken(); //then Assertions.assertEquals(expectedProductAccessToken, actualProductAccessToken); } }
方案2:用PowerMock mock内部构造逻辑(不推荐)
如果你完全不想修改业务代码,需要引入PowerMock对HttpClients.createDefault()静态方法、new HttpPost()构造方法做Mock,让内部生成的对象替换成你准备的Mock实例。这种方式测试代码和业务实现耦合度极高,后续业务代码小改动就可能导致测试失效,非必要不使用。
核心原则:用Mockito做单元测试时,所有需要Mock的依赖都不能在被测方法内部直接new初始化,必须通过注入的方式传入,否则Mockito无法代理到真实执行的对象。
内容的提问来源于stack exchange,提问作者mattsmith5
相关产品推荐
相关产品推荐

