Unit Testing OkHttp

How to easily test OkHttp using MockWebServer

Unit Testing OkHttp

OkHttp is a very powerful HTTP client for Java allowing you to consume RESTful or other resources easily.

In this example below we have a method that sends a request to a service to create a customer.

OkHttp is great as it separates the request from the HTTP call, allowing you flexibility on building a request and executing it.

public CreateCustomerResponse createCustomer(final String jsonBody) throws ServerException {
        final var body = RequestBody.create(MediaType.parse("application/json"), jsonBody);
        final var request = new Request.Builder()
            .url(configuration.getCustomerUrl())
            .post(body)
            .addHeader("API_KEY", configuration.getApiKey())
            .build();

        try (var response = okHttpClient.newCall(request).execute()) {
            var responseBody = response.body();
            var responseString = responseBody != null ? responseBody.string() : null;

            if (StringUtils.isEmpty(responseString)) {
                log.warn("Empty response from Customer service");
                throw new ServerException("Empty response body from Customer service.");
            }

            if (response.isSuccessful()) {
                return objectMapper.readValue(responseString, CreateCustomerResponse.class);
            } else {
                final ServerErrorResponse errorResponse = objectMapper.readValue(responseString, ServerErrorResponse.class);
                log.error("Error response from Customer service : {}", errorResponse);
                throw new ServerException(response.code(), errorResponse.getMessage());
            }
        } catch (final IOException e) {
            log.error("IO Error from Customer service", e);
            throw new ServerException(e.getMessage(), e);
        }
}
Example that creates a customer using a HTTP POST request, checks the response from the service and builds a response.

The ability to build a request separately and then invoke it using the OkHttpClient is a great feature to allow flexibility for developers in their implementation approach.

Testing

The recommended way of testing your code that uses OkHttp is to utilise their MockWebServer utility. This allows the execution of tests in a realistic operation with full control of responses being passed to the client.

The OkHttpClient and Builder are not easy to test using a Mocking framework, which is in part due to its design.  Powermock is possible to use as a workaround, however your mileage may vary.

It is very straightforward to setup a testing scenario using the MockWebServer.

class HttpClientRestImplTest {

    private static final String CUSTOMER_PATH = "/rest/api/v3/customer/";
    public static final String API_KEY = "123456Akjnknk&%";
    private MockWebServer mockWebServer;
    private HttpClientConfiguration configuration;
    private ObjectMapper objectMapper;
    private HttpClient httpClient;

    @BeforeEach
    void setUp() throws IOException {
        mockWebServer = new MockWebServer(); 
        mockWebServer.start(); //initialise mock web server

        URL mockServerBaseUrl = mockWebServer.url("").url(); //get base url of mockwebserver
        URL customerUrl = new URL(mockServerBaseUrl, CUSTOMER_PATH); //append specific url for making request
        
        configuration = mock(HttpClientConfiguration.class);
        
        when(configuration.getCustomerUrl()).thenReturn(customerUrl);
        when(configuration.getApiKey()).thenReturn(API_KEY);

        objectMapper = mock(ObjectMapper.class);
        when(objectMapper.writeValueAsString(any())).thenReturn("{}");

        httpClient = new HttpClientRestImpl(new OkHttpClient(), configuration, objectMapper); //build client with real OkHttpClient
    }
}
Setup of the test class. Note that we are using a real ObjectMapper to deserialize/serialize Java objects and a real OkHttpClient.

In the test setup() method we start an instance of the MockWebServer and get the URL of it. This allows us to configure the HttpClient class, so it sends the requests to the correct location.

Next we create an instance of Jackson's ObjectMapper, so real request and response objects can be de/serialized to and from JSON.

Then we create an instance of the HttpClient we are going to write tests for.

Testing Good Requests and Responses

Building a test is no different than the structure of any other unit test.

@Test
void createCustomer_WhenValidRequestAndValidResponse_ThenReturnResponseObject() throws ServiceException, HttpClientException, JsonProcessingException {

        //Given
        final CreateCustomerRequest request = CreateCustomerRequest.builder().build();
        final String responseBody = "{\"id\":\"123\", \"customerType\":\"Student\"}";
        when(objectMapper.readValue(responseBody, CreateCustomerResponse.class)).thenReturn(new CreateCustomerResponse(123, CustomerType.STUDENT));

        mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody));

        //When
        final var result = httpClient.createCustomer(request);

        //Then
        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(123);
        assertThat(result.getCustomerType()).isEqualTo(CustomerType.STUDENT);
}
Happy path scenario. a valid request and a valid response

The key point is to ensure the mockWebServer object is enqueued with a response for the given scenario. Otherwise no response will be provided when OkHttp makes a real request.

Testing Bad Responses

@Test
void createCustomer_WhenValidCreateCustomerRequestAndEmptyResponse_ThenThrowClientException() {

        //Given
        final CreateCustomerRequest request = CreateCustomerRequest.builder().build();

        mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(""));

        //When
        assertThatThrownBy(() -> httpClient.createCustomer(request))
            .isInstanceOf(ServiceException.class)
            .hasMessage("Empty response body from Customer service");
}
Testing an empty HTTP response body from the service
@Test
void createCustomer_WhenValidCreateCustomerRequestAndErrorResponse_ThenThrowServiceException() throws JsonProcessingException {

        //Given
        final CreateCustomerRequest request = CreateCustomerRequest.builder().build();
        final String responseBody = "{\"message\":\"Server Error\"}";
        when(objectMapper.readValue(responseBody, ErrorResponse.class)).thenReturn(new ErrorResponse("Server Error"));

        mockWebServer.enqueue(new MockResponse().setResponseCode(500).setBody(responseBody));

        //When
        assertThatThrownBy(() -> httpClient.createCustomer(request))
            .isInstanceOf(ServiceException.class)
            .hasMessage("Server Error");
}
Testing a HTTP 500 Service exception response

Testing IOException / Network Failures

The prior scenarios all cover situations where the server sends a positive or negative response. In the production environment this will not always be the case; where a network failure or a HTTP timeout could occur.

Testing these scenarios are straightforward using the MockWebServer's SocketPolicy property.

@Test
void createCustomer_WhenValidCreateCustomerRequestAndIOException_ThenThrowServiceException() throws ServiceException, HttpClientException {

        //Given
        final CreateCustomerRequest request = CreateCustomerRequest.builder().build();

        //When
        mockWebServer.enqueue(new MockResponse()
            .setBody(new Buffer().write(new byte[4096]))
            .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY));

        assertThatThrownBy(() -> httpClient.createCustomer(request))
            .isInstanceOf(ServiceException.class)
            .hasMessage("unexpected end of stream");
}
IOException caused by Network failure
@Test
void createCustomer_WhenValidCreateCustomerRequestAndTimeoutn_ThenThrowServiceException() throws ServiceException, HttpClientException {

        //Given
        final CreateCustomerRequest request = CreateCustomerRequest.builder().build();

        //When
        mockWebServer.enqueue(new MockResponse()
            .setSocketPolicy(SocketPolicy.NO_RESPONSE));

        assertThatThrownBy(() -> httpClient.createCustomer(request))
            .isInstanceOf(ServiceException.class)
            .hasMessage("no response from server");
}
HTTP Timeout exception

This second test is very useful to make sure you have configured your OkHttp client with a proper timeout value. Which is very important for requests that are made in a high volume. The last thing you need to is tie up precious JVM heap-space with multiple requests that are failing to timeout in a timely manner!

Conclusion

The MockWebServer is a great counterpart to OkHttpClient, and allows you to make realistic tests without the hassle of mocking.