使用Mockito嘲弄Apache Post HTTPClient



我正在尝试使用Mockito和本文测试HttpClient。我收到下面的错误,不知道如何修复。错误如下。我看这篇文章的内容非常相似。它在CloseableHttpResponse closeableHttpResponse = client.execute(httpPost)上失败了,当我已经嘲笑它的时候。

资源:使用Mockito mock Apache HTTPClient

主要代码:

public class ProductService {
private final VaultConfig vaultConfig;
private final AppConfig appConfig;
public ProductService(VaultConfig vaultConfig,
@Autowired AppConfig appConfig) {
this.vaultConfig = vaultConfig;
this.appConfig = appConfig;
}
private void createAccessToken() {
String httpUrl = MessageFormat.format("{0}/api/v1/authentication/login",
appConfig.getProductServerUrl());
CloseableHttpClient client = HttpClients.createDefault();
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 = client.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
HttpClients httpClients;
@Mock
CloseableHttpClient closeableHttpClient;
@Mock
HttpPost httpPost;
@Mock
CloseableHttpResponse closeableHttpResponse;
@Mock
private VaultConfig vaultConfig;
@Mock
private AppConfig appConfig;
@BeforeEach
public void initialize() {
MockitoAnnotations.openMocks(this);
productService = new ProductService(vaultConfig, appConfig);
}

void getAccessTokenWhenEmpty() throws IOException {
//given
String expectedProductAccessToken = "ABC";
//and:
given(appConfig.getProductServerUrl()).willReturn("https://test.abcd.com");
given(closeableHttpClient.execute(httpPost)).willReturn(closeableHttpResponse);
given(vaultConfig.getProductAccessToken()).willReturn("");
//when
String actualProductAccessToken = ProductService.getAccessToken();
//then
Assertions.assertEquals(actualProductAccessToken,expectedProductAccessToken);
}

错误:

} catch (Exception e) {
java.net.UnknownHostException: test.abcd.com: unknown error

创建的mock没有在ProductService中使用,因为它们没有在构造中传递。类依赖应该通过构造注入,因为它们是强制性的。

如果您像下面这样更改实现将会有所帮助。代码不完整

public class ProductService {
private final VaultConfig vaultConfig;
private final AppConfig appConfig;
private final CloseableHttpClient client;
public ProductService(VaultConfig vaultConfig,
@Autowired AppConfig appConfig, CloseableHttpClient client) {
this.vaultConfig = vaultConfig;
this.appConfig = appConfig;
}
private void createAccessToken() {
String httpUrl = MessageFormat.format("{0}/api/v1/authentication/login",
appConfig.getProductServerUrl());
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 = client.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 {
@InjectMocks
private ProductService productService;
@Mock
CloseableHttpClient closeableHttpClient;
@Mock
private VaultConfig vaultConfig;
@Mock
private AppConfig appConfig;
@Mock
CloseableHttpResponse closeableHttpResponse;
@Mock
HttpEntity entity
@BeforeEach
public void initialize() {
MockitoAnnotations.openMocks(this);
}

void getAccessTokenWhenEmpty() throws IOException {
//given
String expectedProductAccessToken = "ABC";
//and:
given(appConfig.getProductServerUrl()).willReturn("https://test.abcd.com");
given(closeableHttpClient.execute(httpPost)).willReturn(closeableHttpResponse);
given(vaultConfig.getProductAccessToken()).willReturn("");
// my additional code
given(closeableHttpResponse.getEntity()).willReturn(entity);
given(entity.....).willReturn(....)   // mock the entity for the EntityUtils.toString(entity, "UTF-8");
//when
String actualProductAccessToken = ProductService.getAccessToken();
//then
Assertions.assertEquals(actualProductAccessToken,expectedProductAccessToken);
}

一个更简单、更干净、更不脆弱的方法是不要模仿HttpClient。它只会使你的测试代码具有模式"模拟返回模拟"one_answers"返回的模拟返回另一个模拟",而另一个模拟又返回......";它看起来很丑,对我来说是一种代码味。

相反,使用真正的HTTPClient实例并使用WireMock或MockWebServer等工具模拟外部API

我最喜欢的是MockWebServer,你可以这样做:

public class ProductServiceTest {


private ProductService productService;
@Mock
private AppConfig appConfig;
private MockWebServer server;
@Test
public void getAccessTokenWhenEmpty(){

server.start();
HttpUrl baseUrl = server.url("/api/v1/authentication/login");
given(appConfig.getProductServerUrl()).willReturn("http://" + baseUrl.host() +":" + baseUrl.port());
productService = new ProductService(vaultConfig, appConfig);

//stub the server to return the access token response
server.enqueue(new MockResponse().setBody("{"access_token":"ABC"}"));
//continues execute your test
}

}

如果您不想更改实现,并且不想为创建CloseableHttpClient添加构造函数或附加方法。
您需要模拟静态方法HttpClients.createDefault()!

解决方案1:通过Mockito模拟静态方法
Mockito从3.4.0版本开始支持模拟静态方法。

try (MockedStatic mocked = mockStatic(Foo.class)) {
mocked.when(Foo::method).thenReturn("bar");
assertEquals("bar", Foo.method());
mocked.verify(Foo::method);
}

首先你需要添加mockito-inline到你的项目

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.6.1</version>
<scope>test</scope>
</dependency>

实现:

public class ProductServiceTest {
private ProductService productService;
MockedStatic<HttpClients> httpClientsStaticMock;
@Mock
CloseableHttpClient closeableHttpClient;
@Mock
HttpPost httpPost;
@Mock
CloseableHttpResponse closeableHttpResponse;
@Mock
private VaultConfig vaultConfig;
@Mock
private AppConfig appConfig;
@BeforeEach
public void initialize() {
MockitoAnnotations.openMocks(this);
productService = new ProductService(vaultConfig, appConfig);
//create static Mock
httpClientsStaticMock = mockStatic(HttpClients.class);
//when HttpClients.createDefault() called return mock of CloseableHttpClient
httpClientsStaticMock.when(HttpClients::createDefault).thenReturn(closeableHttpClient);
}
void getAccessTokenWhenEmpty() throws IOException {
//given
String expectedProductAccessToken = "ABC";
//and:
given(appConfig.getProductServerUrl()).willReturn("https://test.abcd.com");
given(closeableHttpClient.execute(httpPost)).willReturn(closeableHttpResponse);
given(vaultConfig.getProductAccessToken()).willReturn("");
//when
String actualProductAccessToken = ProductService.getAccessToken();
//then
Assertions.assertEquals(actualProductAccessToken, expectedProductAccessToken);
}
@AfterEach
public void afterTest() {
//close static Mock
httpClientsStaticMock.close();
}
}

方案2:通过PowerMock模拟静态方法
依赖性:

<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>

实现:

@RunWith(PowerMockRunner.class)
@PrepareForTest({ ProductService.class, HttpClients.class})
public class ProductServiceTest {
private ProductService productService;
@Mock
CloseableHttpClient closeableHttpClient;
@Mock
HttpPost httpPost;
@Mock
CloseableHttpResponse closeableHttpResponse;
@Mock
private VaultConfig vaultConfig;
@Mock
private AppConfig appConfig;
@BeforeEach
public void initialize() {
MockitoAnnotations.openMocks(this);
productService = new ProductService(vaultConfig, appConfig);
//create static Mock
PowerMockito.mockStatic(HttpClients.class);
//when HttpClients.createDefault() called return mock of CloseableHttpClient
when(HttpClients.createDefault()).thenReturn(closeableHttpClient);
}
void getAccessTokenWhenEmpty() throws IOException {
//given
String expectedProductAccessToken = "ABC";
//and:
given(appConfig.getProductServerUrl()).willReturn("https://test.abcd.com");
given(closeableHttpClient.execute(httpPost)).willReturn(closeableHttpResponse);
given(vaultConfig.getProductAccessToken()).willReturn("");
//when
String actualProductAccessToken = ProductService.getAccessToken();
//then
Assertions.assertEquals(actualProductAccessToken, expectedProductAccessToken);
}
}

我将提出一种稍微不同的方法来测试http客户端。单元测试HttpClient具有非常低的ROI,并且可能非常棘手且容易出错。即使您可以覆盖一些简单的阳性情况,也很难测试更复杂的场景,如错误处理、重试。此外,您的测试可能依赖于客户机的内部实现,这在将来可能会发生变化。另外,模拟静态方法通常被认为是一种反模式,应该避免。

我建议你看看WireMock,它为测试web客户端提供了一个非常好的API。下面是一些例子

基本上,你需要启动Wiremock服务器,然后将你的HttpClient指向这个URL

WireMockServer wireMockServer = new WireMockServer(wireMockConfig().dynamicPort());
wireMockServer.start();
WireMock.configureFor(wireMockServer.port());
var baseUrl = "http://localhost:" + wireMockServer.port();
given(appConfig.getProductServerUrl()).willReturn(baseUrl);

之后,您可以定义服务器存根(响应),从您的HttpClient发送实际请求,然后验证响应

stubFor(get("/api")
.willReturn(aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withStatus(200)
.withBody(...)
)
);

您可以通过提供不同的存根轻松地测试正面和负面场景

stubFor(get("/api")
.willReturn(aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withStatus(500)
)
);

此外,您可以使用场景测试重试逻辑,甚至可以使用延迟模拟超时。

相关内容

  • 没有找到相关文章

最新更新