0

I want to write unit test for a method which uses Apache Http to fetch data.

public UserResponse getUserById(String id) {
    UserResponse userResponse;
    String jwtToken = SecurityUtil.getAuthToken();
        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
        HttpGet httpGet = new HttpGet("%s/users/id/%s".formatted(apiUrl, id));

        httpGet.setHeader("Authorization", "Bearer " + jwtToken);
        httpGet.setHeader("Content-Type", "application/json");
        logger.info("network call");
        HttpResponse response = httpClient.execute(httpGet);

        int statusCode = response.getStatusLine().getStatusCode();
        if (response.getStatusLine().getStatusCode() == 200) {
            String responseString = EntityUtils.toString(response.getEntity());
            ObjectMapper mapper = new ObjectMapper();
            mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
            cloneLoanUserResponse = mapper.readValue(responseString, CloneLoanUserResponse.class);
        } else {
            throw new RuntimeException("API call failed with status code: " + statusCode);
        }
    } catch (IOException ex) {
        throw new RuntimeException("Error calling getUserByFinuId due to " + ex);
    }
    return userResponse;
}

Here is the test case that I have managed courtesy chatgpt.

    @Test
    public void shouldReturnUserById() throws IOException {
        UserResponse expectedResponse = new UserResponse(UUID.fromString("ea9a2ace-7872-4c20-ac7f-37d27b5b657b"), "63b2c4f1c3796b6fc01kebab", "1002");

        CloseableHttpClient httpClientMock = Mockito.mock(CloseableHttpClient.class);
        CloseableHttpResponse httpResponseMock = Mockito.mock(CloseableHttpResponse.class);
        HttpEntity httpEntityMock = mock(HttpEntity.class);
        StatusLine statusLineMock = mock(StatusLine.class);

        when(httpClientMock.execute(any(HttpGet.class))).thenReturn(httpResponseMock);
        when(httpResponseMock.getStatusLine()).thenReturn(statusLineMock);
        when(statusLineMock.getStatusCode()).thenReturn(200);
        when(httpResponseMock.getEntity()).thenReturn(httpEntityMock);
        when(httpEntityMock.getContent()).thenReturn(new ByteArrayInputStream("{\n  \"userId\": \"ea9a2ace-7872-4c20-ac7f-37d27b5b657b\",\n  \"legacyId\": \"645606f1bd069d25b6d0ce59\",\n  \"finuId\": \"645606f1bd069d25b6d0ce59\"\n}".getBytes()));

        // Mock ObjectMapper.readValue()
        ObjectMapper objectMapperMock = mock(ObjectMapper.class);
        when(objectMapperMock.readValue(anyString(), eq(CloneLoanUserResponse.class))).thenReturn(expectedResponse);
        try {
            when(httpClientMock.execute(any(HttpGet.class))).thenReturn(httpResponseMock);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // When
        CloneLoanUserResponse actualResponse = userServiceClient.getUserByFinuId("FINUA0001002");

        // Then
        // Add assertions based on your expected and actual results
        assertEquals(expectedResponse, actualResponse);
    }

When I try to run above test case, it makes an http call on a random port but fails to return anything and complete / exit the test.

I am very new to Java and Junit. Help is appreciated.

1 Answer 1

1

Your mocking is unfortunately not used, because the method to test creates its own CloseableHttpClient. Fortunately, HttpClient has something for this. With these, you can send actual HTTP requests, but to a test server that only exists while each test is running.

HttpClient 4.x

Since the tags include "apache-httpclient-4.x" I'll that with that.

First you need a dependency with the same version as the HttpClient you're using:

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <classifier>tests</classifier>
            <scope>test</scope>
        </dependency>

This gives you access to class LocalServerTestBase. This is meant to be extended by your own test classes. If that's not possible you need to create a little helper class that exposes a protected field you need:

final class HttpServer extends LocalServerTestBase {

    ServerBootstrap serverBootStrap() {
        return serverBootstrap;
    }
}

If you're using JUnit 4, the LocalServerTestBase comes with its own @Before and @After methods. If you're using JUnit 5 you need to call these explicitly:

// if needed, include this
//private Server server = new Server();

@BeforeEach
protected void startServer() {
    // The setUp method is annotated with JUnit 4's @Before
    assertDoesNotThrow(this::setUp);
}

@AfterEach
protected void shutdownServer() {
    // The setUp method is annotated with JUnit 4's @After
    assertDoesNotThrow(this::shutDown);
}

In your test methods you can now register a handler that determines the response:

// use server.serverBootStrap() if you didn't extend LocalServerTestBase
serverBootStrap.registerHandler("/path-to-test", (request, response, context -> {
    // setup your response here
    response.setStatusCode(200);
});

After that's done, you must call start() (or server.start()). That returns an HttpHost that you can use to get its port etc. It has method toURI that gives you a URI if you prefer that.

HttpHost host = assertDoesNotThrow(this::start);

Now that you have that, you can set the apiUrl of your userServiceClient.

If you need the host for the apiUrl in your existing @BeforeEach method, you can move some stuff around so you can call server.start() there. That does mean setting up the handler in your @BeforeEach method. That makes it less flexible.

HttpClient 5.x

HttpClient 5.x has something similar. You again need a dependency with the same version as the HttpClient you're using:

        <dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5-testing</artifactId>
            <scope>test</scope>
        </dependency>

Unlike HttpClient 4'x LocalServerTestBase, HttpClient 5's ClassicTestServer was always meant to be included. And unlike localServerTestBase, you can register handlers after starting. That makes it simpler to use.

Setup this server in your test class:

private ClassicTestServer server = new ClassicTestServer();
private String hostname;

@BeforeEach
protected void start(TestInfo testInfo) {
    assertDoesNotThrow(() -> server.start());

    InetAddress address = server.getInetAddress();
    // you can now get the port and hostname for the apiUrl from address
    hostname = address.getHostAddress();
}

@AfterEach
protected void shutdown() {
    server.shutdown(CloseMode.GRACEFUL);
}

In your test methods you can now register a handler that determines the response:

server.registerHandlerVirtual(hostname, "/path-to-test", (request, response, context -> {
    // setup your response here
    response.setCode(200);
});
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you Rob. Do you have any working example of the same for reference? I am getting this error java.lang.NullPointerException: Cannot invoke "org.apache.http.impl.bootstrap.ServerBootstrap.registerHandler(String, org.apache.http.protocol.HttpRequestHandler)" because "this.serverBootstrap" is null
Did you make sure that the setUp method is called? Because that's what should set the serverBootstrap variable. As I said, in JUnit 4 it should get called automatically, in JUnit 5 you need to call it yourself.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.