Skip to content

Instantly share code, notes, and snippets.

@ledoyen
Last active March 20, 2023 18:57
Show Gist options
  • Save ledoyen/4316c7ba4f1e69fcf51083c975571570 to your computer and use it in GitHub Desktop.
Save ledoyen/4316c7ba4f1e69fcf51083c975571570 to your computer and use it in GitHub Desktop.
Binding HttpClient to MockMvc

Given two interfaces such as

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpClientErrorException;

public interface HttpClient<OUTPUT> {

    ResponseEntity<OUTPUT> call(HttpMethod httpMethod, String resource, HttpEntity<?> input) throws HttpClientErrorException;

    default ResponseEntity<OUTPUT> call(HttpMethod httpMethod, String resource, Object body) throws HttpClientErrorException {
        return call(httpMethod, resource, new HttpEntity<>(body));
    }

    default OUTPUT post(String resource, Object body) throws HttpClientErrorException {
        return call(HttpMethod.POST, resource, new HttpEntity<>(body)).getBody();
    }
}

and

import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.client.RestTemplate;

public interface HttpClientFactory {

    <OUTPUT> HttpClient<OUTPUT> create(RestTemplate restTemplate, String url, ParameterizedTypeReference<OUTPUT> responseType);
}

A basic implementation could be

import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.client.RestTemplate;

public <OUTPUT> HttpClient<OUTPUT> create(RestTemplate restTemplate, String url, ParameterizedTypeReference<OUTPUT> responseType) {
    return (httpMethod, resource, input) -> {
        return restTemplate.exchange(url + resource, httpMethod, input, responseType);
    };
}

Then the following utility can be used to bind calls between an HttpClientFactory and a MockMvc:

import com.fasterxml.jackson.databind.ObjectMapper;

import org.mockito.Mockito;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public final class MockMvcBinder {

    private final HttpClientFactory httpClientFactory;

    private MockMvcBinder(HttpClientFactory httpClientFactory) {
        this.httpClientFactory = httpClientFactory;
    }

    /**
     * @param httpClientFactory mock to bind to
     * @return a {@link MockMvcBinder} dedicated to bind the given {@link HttpClientFactory} mock to a {@link MockMvc}
     */
    public static MockMvcBinder bind(HttpClientFactory httpClientFactory) {
        return new MockMvcBinder(httpClientFactory);
    }

    public <T> void  to(MockMvc target) {
        to(() -> target);
    }

    public <T> void  to(Supplier<MockMvc> target) {
        ObjectMapper om = new ObjectMapper();

        when(httpClientFactory.create(any(), any(), any())).then(iom -> {
            ParameterizedTypeReference targetType = iom.getArgument(1);
            HttpClient httpClient = mock(HttpClient.class, CALLS_REAL_METHODS);
            stubHttpClient(httpClient, target, targetType.getType());
            return httpClient;
        });
    }

    private void stubHttpClient(HttpClient mock, Supplier<MockMvc> target, Type targetType) {
        ObjectMapper om = new ObjectMapper();
        when(mock.call(any(), any(), any(HttpEntity.class))).then(iom -> {
            HttpMethod method = iom.getArgument(0);
            String path = iom.getArgument(1);
            HttpEntity entity = iom.getArgument(2);

            MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.request(method, path);
            requestBuilder.headers(entity.getHeaders());
            requestBuilder.contentType(Optional.ofNullable(entity.getHeaders().getContentType()).orElse(MediaType.APPLICATION_JSON_UTF8));
            Optional
                    .ofNullable(entity.getBody())
                    .map(ThrowingFunction.silent(body -> om.writeValueAsString(body)))
                    .ifPresent(requestBuilder::content);

            MvcResult result = target.get().perform(requestBuilder).andReturn();
            HttpStatus status = HttpStatus.valueOf(result.getResponse().getStatus());
            HttpHeaders headers = getResponseHeaders(result.getResponse());
            if(!status.is2xxSuccessful()) {
                handleError(status, headers, result.getResponse());
            }

            Object body = om.readValue(result.getResponse().getContentAsString(), om.getTypeFactory().constructType(targetType));

            return new ResponseEntity(body, headers, status);
        });
    }

    public void handleError(HttpStatus statusCode, HttpHeaders headers, MockHttpServletResponse response) throws IOException {
        switch (statusCode.series()) {
            case CLIENT_ERROR:
                throw new HttpClientErrorException(statusCode, statusCode.getReasonPhrase(),
                        headers, response.getContentAsByteArray(), null);
            case SERVER_ERROR:
                throw new HttpServerErrorException(statusCode, statusCode.getReasonPhrase(),
                        headers, response.getContentAsByteArray(), null);
            default:
                throw new RestClientException("Unknown status code [" + statusCode + "]");
        }
    }

    private HttpHeaders getResponseHeaders(MockHttpServletResponse response) {
        HttpHeaders headers = new HttpHeaders();
        response.getHeaderNames().forEach(name -> headers.put(name, response.getHeaders(name)));
        return headers;
    }
}

Using it will be simple as :

HttpClientFactory httpClientFactory = mock(HttpClientFactory.class);
MockMvc mockMvc = ...
// calls to clients created from the Factory will be redirected to the targetMockMvc instance
MockMvcBinder.bind(httpClientFactory).to(mockMvc);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment