1

I am trying to test HttpClient using Mockito and this article. I am receiving the error below, and not sure how to fix. The error is below. I am following the contents of article very similarly. Its failing on CloseableHttpResponse closeableHttpResponse = client.execute(httpPost) , when I already mocked it.

Resource: Mocking Apache HTTPClient using Mockito

Main Code:

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());
        }
    }

Test Attempt:

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);
    }

Error:

    } catch (Exception e) {

java.net.UnknownHostException: test.abcd.com: unknown error
mattsmith5
  • 540
  • 4
  • 29
  • 67

4 Answers4

2

The created mocks aren't used in the ProductService because they aren't passed in the construct. The class dependencies should be injected through the construct because they are mandatory.

It would help if you changed the implementation like the following. The code is incomplete.

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);
    }
  • yeah, I was thinking about that, however, it seems like I am making unnecessary constructors in the software code, just for testing, when not needed , is there anyway around this? thanks – mattsmith5 Jun 16 '22 at 19:31
  • It was a pleasure. The construct args are necessary to use the dependencies injection (DI) in your program; the DI take you many advantages. If in your program, you are using Spring framework or Juice, you can use '@Inject' or '@Autowired' annotation without the constructors. – Roberto Foglia Jun 16 '22 at 20:16
  • @mattsmith5 it's not unusual that you have to create additional entry paths to your code in order to make it testable. If you do not want to expose the code specific for testing, you can make it `protected` or `package private`. – slindenau Jun 23 '22 at 07:06
1

A easier, cleaner and less fragile approach is don't mock the HttpClient. It just causes your test codes to have the pattern "mock return mock and the returned mock return anther mock which in turn return another mock which return ......" which looks very ugly and is a code smell to me.

Instead , use a real HTTPClient instance and mock the external API using tools likes WireMock or MockWebServer

My favourite is MockWebServer which you can do something likes:

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

    }


}
Ken Chan
  • 84,777
  • 26
  • 143
  • 172
0

In case you do not want to change the implementation, and do not want to add the constructor or additional method for creation CloseableHttpClient.
You need to mock static method HttpClients.createDefault()!

Solution 1: mock static method via Mockito
Mockito since 3.4.0 version support mocking static methods.

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

First of all you need to add mockito-inline to your project

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

Implementation:

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();
    }
}

Solution 2: mock static methods via PowerMock
Dependencies:

        <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>

Implementation:

@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);
    }
}
Eugene
  • 5,269
  • 2
  • 14
  • 22
0

I would propose a little bit different approach for testing http clients. Unit testing HttpClient has very low ROI and could be very tricky and error-prone. Even if you could cover some simple positive cases, it would be very hard to test more complex scenarios, like error handling, retries. Also your tests could depend on the internal implementation of the client that could change in the future. Also, mocking static methods usually considered as an anti-pattern and should be avoided.

I would recommend to look at WireMock which provides a very good API for testing web clients. Here are some examples

Basically, you need to start Wiremock server and then point your HttpClient to this URL

WireMockServer wireMockServer = new WireMockServer(wireMockConfig().dynamicPort());
wireMockServer.start();
WireMock.configureFor(wireMockServer.port());

var baseUrl = "http://localhost:" + wireMockServer.port();

given(appConfig.getProductServerUrl()).willReturn(baseUrl);

After that you could define server stubs (responses), send real requests from your HttpClient and then validate responses

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

You could easily test both positive and negative scenarios by providing different stubs

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

Also, you could test retry logic using Scenarios or even simulate timeouts using Delays.

Alex
  • 4,987
  • 1
  • 8
  • 26