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