Can anyone show me an example, how to test RestTemplate using Spock.
My classes looks like this:
#Service
public class SomeService {
#Autowired
private EndpointUrlProvider endpointUrlProvider;
private RestTemplate restTemplate = new RestTemplate();
public SomeResponse doSomePostRequest(HttpEntity<?> httpEntity) throws Exception {
ResponseEntity<SomeResponse> response;
try{
response = restTemplate.postForEntity(endpointUrlProvider.getSomeUrl(),httpEntity,SomeResponse.class);
} catch (Exception e){
throw new Exception("Exception occured during post for:" + httpEntity.getBody().getClass().getSimpleName() + " Cause: " + e.getMessage());
}
if(response.getStatusCode() == HttpStatus.OK){
return response.getBody();
}
throw new Exception("Error during " + response.getBody().getClass().getSimpleName() + "Http status is diffrent than 200: " + response.getBody().toString());
}
}
Tests:
class SomeTest extends Specification {
RestTemplate restTemplate = Mock {
postForEntity(_, _, SomeResponse.class) >> new ResponseEntity(new SomeResponse(), HttpStatus.OK)
}
#Autowired
SomeService someService
def "someTest"() {
when:
SomeResponse someResponse = someService.doSomePostRequest(new HttpEntity<>(new SomeBody(), new HttpHeaders()))
then:
someResponse == new SomeResponse()
}
}
The major problem is mocking behaviour of RestTemplate,I looking for solution how to do it in proper way. I'm dont use spring boot.
The situation is following:
You create new RestTemplate in your service class.
private RestTemplate restTemplate = new RestTemplate();
Then you create mock in tests and call your service method:
RestTemplate restTemplate = Mock {
postForEntity(_, _, SomeResponse.class) >> new ResponseEntity(new SomeResponse(), HttpStatus.OK)
}
....
someService.doSomePostRequest
But your service still has usual rest template inside. You should inject mock object. I would suggest you doing it through constructor. So change your code to:
#Service
public class SomeService {
private EndpointUrlProvider endpointUrlProvider;
private RestTemplate restTemplate;
#Autowired
public SomeService(EndpointUrlProvider endpointUrlProvider, RestTemplate restTemplate){
this.endpointUrlProvider = endpointUrlProvider;
this.restTemplate = restTemplate;
}
And your test will be:
class SomeTest extends Specification {
RestTemplate restTemplate = Mock {
postForEntity(_, _, SomeResponse.class) >> new ResponseEntity(new SomeResponse(), HttpStatus.OK)
}
SomeService someService = new SomeService ( null, restTemplate);
def "someTest"() {
when:
SomeResponse someResponse = someService.doSomePostRequest(new HttpEntity<>(new SomeBody(), new HttpHeaders()))
then:
someResponse == new SomeResponse()
}
}
Now your service object will call method on injected MOCK, not usual RestTemplate
p.s.
Constructor injection considered to be a good practice by spring.
It's better to create a RestTemplate bean and inject it everywhere, than create new object in all services.
Related
I am working on a non-spring application and using restTemplate feature from spring-web.While writing Junit test i am unable to mock response from restTemplate.postForEntity(). What am i doing wrong here.
Below is the function
public JsonObject apiResponse(Request request) throws JsonProcessingException {
String queryRequest = objectMapper.writeValueAsString(request);
HttpHeaders headers = new HttpHeaders();
headers.add("content-type", "application/json");
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.postForEntity(
"https://testurl/test/",
new HttpEntity<>(queryRequest, headers), String.class);
return JsonParser.parseString(Objects.requireNonNull(responseEntity.getBody())).getAsJsonObject();
Below is the Junit
#ExtendWith(MockitoExtension.class)
public class HandlerTest {
#Spy
#InjectMocks
Handler handler;
#Mock
RestTemplate restTemplate;
#Test
public void apiREsponseTest() throws JsonProcessingException {
//this is not working
Mockito.doReturn(new ResponseEntity <>("test", HttpStatus.OK))
.when(restTemplate).postForEntity(eq("test"), eq(HttpEntity.class), eq(String.class));
assertNotNull(handler.apiResponse(request));
RestTemplate restTemplate = new RestTemplate();
This is not getting mocked as it's a local object and it will always call the real method. You can try this to mock the restTemplate:
RestTemplate restTemplate = getRestTemplate();
protected RestTemplate getRestTemplate() {
return new RestTemplate();
}
//Add this stub to your test
when(handler.getRestTemplate()).thenReturn(restTemplate);
The URL is hardcoded here:
ResponseEntity<String> responseEntity = restTemplate.postForEntity(
"https://testurl/test/",
new HttpEntity<>(queryRequest, headers), String.class);
Hence the you need to change your stubbing as well.
Mockito.doReturn(new ResponseEntity <>("test", HttpStatus.OK))
.when(restTemplate).postForEntity(eq("https://testurl/test/"), eq(HttpEntity.class), eq(String.class));
Making these changes should make your test green hopefully. Let me know if this doesn't resolve your issue.
first of all I try to write unit-test for remote service to get weather and using restTemplate then implement ClientHttpRequestInterceptor to get remote request && remote response for logging purpose now i try to mock restTemplate to write unit-test and get the following error
When using matchers, all arguments have to be provided by matchers.
For example:
//correct:
someMethod(anyObject(), eq("String by matcher"));
For more info see javadoc for Matchers class.
and here you here code i wrote for unit-test
#RunWith(MockitoJUnitRunner.class)
public class WeatherRemoteServiceTest {
#Mock CustomResttemplate restTemplate;
#Mock WeatherRemoteUtilies weatherUtilies;
#InjectMocks WeatherRemoteService weatherRemote;
#Test
public void testRetrieveWeather() {
ResponseEntity<String> MockResponse= new ResponseEntity<String>(HttpStatus.OK);
Mockito.when(weatherUtilies.buildRequestParams(Mockito.anyString()))
.thenReturn(Mockito.any(MultiValueMap.class));
ResponseEntity<String> responseEntity = new ResponseEntity<String>("sampleBodyString", HttpStatus.OK);
Mockito.when(restTemplate.buildRestTemplate().exchange(
Matchers.anyString(),
Matchers.any(HttpMethod.class),
Matchers.<HttpEntity<?>> any(),
Matchers.<Class<String>> any()
)
).thenReturn(responseEntity);
assertEquals(weatherRemote.retrieveWeather("ciaro").getStatusCode(), HttpStatus.OK);
}
}
and here is code for business logic itself
#Service
public class WeatherRemoteService {
private final Logger logger= LoggerFactory.getLogger(this.getClass());
#Value("${openweather.url}")
private String url;
#Autowired
private WeatherRemoteUtilies weatherUtilies;
#Autowired
private CustomResttemplate customRestTemplate;
public ResponseEntity<?> retrieveWeather(String city) {
logger.info(Constants.CLASS_NAME+this.getClass().getName()+Constants.METHOD_NAME+new Object() {}.getClass().getEnclosingMethod().getName());
logger.debug(Constants.METHOD_ARGUMENTS+city);
RestTemplate restRequest= customRestTemplate.buildRestTemplate();
HttpHeaders headers= new HttpHeaders();
headers.set("Accept",MediaType.APPLICATION_JSON_UTF8_VALUE);
UriComponentsBuilder uri= UriComponentsBuilder.fromUriString(url).
path("/data/2.5/weather")
.queryParams(weatherUtilies.buildRequestParams(city));
HttpEntity<String>entity= new HttpEntity<>(headers);
ResponseEntity<String>WeatherResponse=restRequest.exchange(uri.toUriString(), HttpMethod.GET, entity, String.class);
logger.info(Constants.END_METHOD);
return WeatherResponse;
}
}
code for RestTemplateInterceptor
public class RestTemplateInterceptor implements ClientHttpRequestInterceptor {
private final Logger logger =LoggerFactory.getLogger(this.getClass());
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
logRequest(request, body);
ClientHttpResponse httpResponse= execution.execute(request, body);
logResponse(httpResponse);
return httpResponse;
}
private void logRequest(HttpRequest request, byte[] body) throws IOException {
logger.info(Constants.START_REMOTE_REQUEST);
logger.info("URI :{}", request.getURI().toString());
logger.info("Method :{}", request.getMethod().toString());
logger.info("Headers :{}", request.getHeaders().toString());
logger.info("Request body :{}", new String(body,"UTF-8").toString());
logger.info(Constants.END_REMOTE_REQUEST);
}
private void logResponse(ClientHttpResponse response) throws IOException {
logger.info(Constants.START_REMOTE_RESPONSE);
logger.info("Status code :{}", response.getStatusCode().toString());
logger.info("Status text :{}", response.getStatusText().toString());
logger.info("Headers :{}", response.getHeaders().toString());
logger.info("Response body :{}", StreamUtils.copyToString(response.getBody(), Charset.defaultCharset()));
logger.info(Constants.END_REMOTE_RESPONSE);
}
}
then setInterceptor on RestTemplate factory using the following code
public class CustomResttemplate {
public RestTemplate buildRestTemplate() {
SimpleClientHttpRequestFactory simpleFactory= new SimpleClientHttpRequestFactory();
simpleFactory.setOutputStreaming(false);
ClientHttpRequestFactory factory = new BufferingClientHttpRequestFactory(simpleFactory);
RestTemplate restTemplate= new RestTemplate(factory);
restTemplate.setInterceptors(Collections.singletonList(new RestTemplateInterceptor()));
return restTemplate;
}
}
Your problem is where you write
.thenReturn(Mockito.any(MultiValueMap.class))
You actually have to tell Mockito what to return. You can't use a matcher here. Matchers are for verifying, and for setting up what conditions to stub. You can't use them to tell Mockito what to return from a stubbed call.
Create a specific MultiValueMap to pass to thenReturn.
I am currently writing unit test for below method
#Autowired
private RequestConfig requestConfig;
#Autowired
private RetryTemplate retryTemplate;
public ResponseEntity<String> makeGetServiceCall(String serviceUrl) throws Exception {
try {
return retryTemplate.execute(retryContext -> {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = requestConfig.createHttpHeaders();
HttpEntity<String> entity = new HttpEntity<String>("parameters", headers);
ResponseEntity<String> response = restTemplate.exchange(serviceUrl, HttpMethod.GET, entity, String.class);
return response;
});
} catch (Exception e) {
throw new Exception("Generic exception while makeGetServiceCall due to" + e + serviceUrl);
}
}
UPDATED METHOD:
#Autowired
private RequestConfig requestConfig;
#Autowired
private RetryTemplate retryTemplate;
#Autowired
private RestTemplate restTemplate;
public ResponseEntity<String> makeGetServiceCall(String serviceUrl) throws Exception {
try {
return retryTemplate.execute(retryContext -> {
HttpHeaders headers = requestConfig.createHttpHeaders();
HttpEntity<String> entity = new HttpEntity<String>("parameters", headers);
ResponseEntity<String> response = restTemplate.exchange(serviceUrl, HttpMethod.GET, entity, String.class);
return response;
});
} catch (Exception e) {
throw new Exception("Generic exception while makeGetServiceCall due to" + e + serviceUrl);
}
}
I tried all possibilities but I am unable to get it right. Here is my below test.
#Mock
private RestTemplate restTemplate;
#Mock
public RequestConfig requestConfig;
#InjectMocks
private RetryTemplate retryTemplate;
ServiceRequest serviceRequest;
#Test
public void makeGetServiceCall() throws Exception {
String url = "http://localhost:8080";
RetryTemplate mockRetryTemplate = Mockito.mock(RetryTemplate.class);
RestTemplate mockRestTemplate = Mockito.mock(RestTemplate.class);
ResponseEntity<String> myEntity = new ResponseEntity<>(HttpStatus.ACCEPTED);
Mockito.when(mockRetryTemplate.execute(ArgumentMatchers.any(RetryCallback.class), ArgumentMatchers.any(RecoveryCallback.class), ArgumentMatchers.any(RetryState.class))).thenReturn(myEntity);
Mockito.when(mockRestTemplate.exchange(
ArgumentMatchers.eq(url),
ArgumentMatchers.eq(HttpMethod.GET),
ArgumentMatchers.<HttpEntity<String>>any(),
ArgumentMatchers.<Class<String>>any())
).thenReturn(myEntity);
ResponseEntity<String> response = serviceRequest.makeGetServiceCall(url);
Assert.assertEquals(myEntity, response);
}
UPDATED TEST CASE:
#Mock
public RequestConfig requestConfig;
#Mock
private RestTemplate restTemplate;
#Mock
private RetryTemplate retryTemplate;
#InjectMocks
ServiceRequest serviceRequest;
#Test
public void makeGetServiceCall() throws Exception {
//given:
String url = "http://localhost:8080";
when(requestConfig.createHttpHeaders()).thenReturn(null);
ResponseEntity<String> myEntity = new ResponseEntity<>( HttpStatus.ACCEPTED);
when(retryTemplate.execute(any(RetryCallback.class), any(RecoveryCallback.class), any(RetryState.class))).thenAnswer(invocation -> {
RetryCallback retry = invocation.getArgument(0);
return retry.doWithRetry(/*here goes RetryContext but it's ignored in ServiceRequest*/null);
});
when(restTemplate.exchange(anyString(), any(HttpMethod.class), any(HttpEntity.class), eq(String.class)))
.thenReturn(myEntity);
//when:
ResponseEntity<String> response = serviceRequest.makeGetServiceCall(url);
//then:
assertEquals(myEntity, response);
}
The response object which I get from my method call makeGetServiceCall always return null. When I debug the code I see exception org.mockito.exceptions.misusing.WrongTypeOfReturnValue: ResponseEntity cannot be returned by toString() toString() should return String error on the resttemplate mocking where I return myEntity
I am not sure what am I missing.
Well, you have made quite some number of mistakes...
I'm sure you wanted to annotate private RetryTemplate retryTemplate; with #Mock, not #InjectMocks
#InjectMocks should go onto ServiceRequest serviceRequest;
You are defining interactions on some mockRetryTemplate and mockRestTemplate which have nothing to do with serviceRequest. Instead, you should use your #Mock-annotated fields to define interactions on because they are being injected into your object under test (serviceRequest)
Moreover, you can't normally mock RestTemplate and inject it into your ServiceRequest because you don't use dependency injection in the first place for RestTemplate in ServiceRequest. You just instantiate its instance in ServiceRequest.makeGetServiceCall
You are defining an interaction on the wrong method at line Mockito.when(retryTemplate.execute(.... Your interaction specifies RetryTemplate.execute(RetryCallback, RecoveryCallback, RetryState) whereas your ServiceRequest uses another method RetryTemplate.execute(RetryCallback)
You should also notice that RetryTemplate.execute is final and so you can't mock it without extra efforts as explained here. And generally, you should prefer interfaces over classes, e.g. RestOperations and RetryOperations over RestTemplate and RetryTemplate respectively, to be more flexible.
That said, below is the working test which solves your problem. But take note of removing RestTemplate restTemplate = new RestTemplate(); from ServiceRequest and making restTemplate a field so it's dependency-injected.
#RunWith(MockitoJUnitRunner.class)
public class ServiceRequestTest {
#Mock
private RestTemplate restTemplate;
#Mock
public RequestConfig requestConfig;
#Mock
private RetryTemplate retryTemplate;
#InjectMocks
ServiceRequest serviceRequest;
#Test
public void makeGetServiceCall() throws Exception {
//given:
String url = "http://localhost:8080";
ResponseEntity<String> myEntity = new ResponseEntity<>(HttpStatus.ACCEPTED);
when(retryTemplate.execute(any(RetryCallback.class))).thenAnswer(invocation -> {
RetryCallback retry = invocation.getArgument(0);
return retry.doWithRetry(/*here goes RetryContext but it's ignored in ServiceRequest*/null);
});
when(restTemplate.exchange(eq(url), eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class)))
.thenReturn(myEntity);
//when:
ResponseEntity<String> response = serviceRequest.makeGetServiceCall(url);
//then:
assertEquals(myEntity, response);
}
}
For me below worked, otherwise, it was returning null always
when(retryTemplate.execute(any(),any(),any())).thenAnswer(invocation -> {
RetryCallback retry = invocation.getArgument(0);
return retry.doWithRetry(null);
});
and import was
import static org.mockito.ArgumentMatchers.any;
Generic solution:
Mockito.when(retryTemplate.execute(Matchers.any(),Matchers.any(),Matchers.any())).thenAnswer(invocation -> {
RetryCallback retry = invocation.getArgumentAt(0,Matchers.any());
return retry.doWithRetry(null);
});
It works for me!
#ExtendWith(MockitoExtension.class)
class RetryableRestClientTest {
#Mock
private RestTemplate restTemplate;
#Mock
private RetryTemplate retryTemplate;
#InjectMocks
private RetryableRestClient client;
#SuppressWarnings("rawtypes")
#Test
void test_get() {
String url = "https://faked-url";
ResponseEntity<String> expectedResponseEntity = new ResponseEntity<>(HttpStatus.OK);
Mockito.when(retryTemplate.execute(Mockito.any(), Mockito.any(), Mockito.any()))
.thenAnswer(invocation -> {
RetryCallback retry = invocation.getArgument(0);
return retry.doWithRetry(null);
});
Mockito.when(restTemplate.exchange(Mockito.eq(url), Mockito.eq(HttpMethod.GET), Mockito.any(HttpEntity.class), Mockito.eq(String.class)))
.thenReturn(expectedResponseEntity);
ResponseEntity<String> actualResponseEntity = client.get(url);
Assertions.assertEquals(expectedResponseEntity, actualResponseEntity);
}
}
#Component
public class RetryableRestClient {
#Autowired
private RetryTemplate retryTemplate;
#Autowired
private RestTemplate restTemplate;
private HttpHeaders fakeHttpHeaders() {
HttpHeaders headers = new HttpHeaders();
// fake browser's behavior
headers.add("authority", "m.nowscore.com");
headers.add("cache-control", "max-age=0");
headers.add("sec-ch-ua", "\" Not;A Brand\";v=\"99\", \"Google Chrome\";v=\"91\", \"Chromium\";v=\"91\"");
headers.add("sec-ch-ua-mobile", "?0");
headers.add("upgrade-insecure-requests", "1");
headers.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36");
headers.add("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9");
headers.add("sec-fetch-site", "none");
headers.add("sec-fetch-mode", "navigate");
headers.add("sec-fetch-user", "?1");
headers.add("sec-fetch-dest", "document");
headers.add("accept-language", "en-US,en;q=0.9");
return headers;
}
public final ResponseEntity<String> get(String url) {
return retryTemplate.execute(context -> restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, fakeHttpHeaders()), String.class));
}
}
I want to mock the resttemplate call which is instansiated as local variable and exchange method invoked. I mocked using expectation but it invokes the actual method . Am I missing something . Please help me on this . Thanks in advance
public class ServiceController {
public String callGetMethod (HttpServletRequest request){
String url = request.getParameter("URL_TO_CALL");
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
HttpEntity<String> entity = new HttpEntity<String>(headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
return res.getBody();
}
}
#RunWith(JMockit.class)
public class ServiceControllerTest {
#Tested
private ServiceController controller;
#Test
public void callGetMethod (#Mocked HttpServletRequest request, #Mocked RestTemplate restTemplate){
new NonStrictExpectations() {
{
restTemplate.exchange(host,HttpMethod.GET, entity,String.class); returns (new ResponseEntity<String>("success" , HttpStatus.OK));
}
ResponseEntity<String> response = controller.callGetMethod(httpServletRequest);
}
}
We need to mock the new RestTemplate() . So that it will assign mocked object restTemplate to method local variable .
#Mocked
RestTemplate restTemplate;
new NonStrictExpectations() {
{
new RestTemplate();
result = restTemplate;
restTemplate.exchange(host, HttpMethod.GET, entity, String.class);
returns(new ResponseEntity<String>("success", HttpStatus.OK));
}
I have a REST controller:
#RequestMapping(value = "greeting", method = RequestMethod.GET, produces = "application/json; charset=utf-8")
#Transactional(readOnly = true)
#ResponseBody
public HttpEntity<GreetingResource> greetingResource(#RequestParam(value = "message", required = false, defaultValue = "World") String message) {
GreetingResource greetingResource = new GreetingResource(String.format(TEMPLATE, message));
greetingResource.add(linkTo(methodOn(AdminController.class).greetingResource(message)).withSelfRel());
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add("Content-Type", "application/json; charset=utf-8");
return new ResponseEntity<GreetingResource>(greetingResource, responseHeaders, HttpStatus.OK);
}
As you can see, I'm trying hard to specify the content type returned by the controller.
It is accessed with a REST client:
public String getGreetingMessage() {
String message;
try {
HttpHeaders httpHeaders = Common.createAuthenticationHeaders("stephane" + ":" + "mypassword");
ResponseEntity<GreetingResource> responseEntity = restTemplate.getForEntity("/admin/greeting", GreetingResource.class, httpHeaders);
GreetingResource greetingResource = responseEntity.getBody();
message = greetingResource.getMessage();
} catch (HttpMessageNotReadableException e) {
message = "The GET request FAILED with the message being not readable: " + e.getMessage();
} catch (HttpStatusCodeException e) {
message = "The GET request FAILED with the HttpStatusCode: " + e.getStatusCode() + "|" + e.getStatusText();
} catch (RuntimeException e) {
message = "The GET request FAILED " + ExceptionUtils.getFullStackTrace(e);
}
return message;
}
The http headers are created by a utility:
static public HttpHeaders createAuthenticationHeaders(String usernamePassword) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
byte[] encodedAuthorisation = Base64.encode(usernamePassword.getBytes());
headers.add("Authorization", "Basic " + new String(encodedAuthorisation));
return headers;
}
The web security configuration and code work fine. I make sure of this using a mockMvc based integration test which succeeds.
The only test that fails is the one based on the REST template:
#Test
public void testGreeting() throws Exception {
mockServer.expect(requestTo("/admin/greeting")).andExpect(method(HttpMethod.GET)).andRespond(withStatus(HttpStatus.OK));
String message = adminRestClient.getGreetingMessage();
mockServer.verify();
assertThat(message, allOf(containsString("Hello"), containsString("World")));
}
The exception given in the Maven build console output is:
java.lang.AssertionError:
Expected: (a string containing "Hello" and a string containing "World")
got: "The GET request FAILED org.springframework.web.client.RestClientException : Could not extract response: no suitable HttpMessageConverter found for response type [class com.thalasoft.learnintouch.rest.resource.GreetingR esource] and content type [application/octet-stream]\n\tat org.springframework.web.client.HttpMessageConverte rExtractor.extractData(HttpMessageConverterExtract or.java:107)
I'm using the Spring Framework 3.2.2.RELEASE version and the Spring Security 3.1.4.RELEASE version on the Java 1.6 version.
At first, I had a bare bone REST template:
#Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
I have now added to it, hoping it would help:
private static final Charset UTF8 = Charset.forName("UTF-8");
#Bean
public RestTemplate restTemplate() {
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "json", UTF8)));
messageConverters.add(mappingJackson2HttpMessageConverter);
Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
jaxb2Marshaller.setClassesToBeBound(new Class[] {
GreetingResource.class
});
MarshallingHttpMessageConverter marshallingHttpMessageConverter = new MarshallingHttpMessageConverter(jaxb2Marshaller, jaxb2Marshaller);
messageConverters.add(marshallingHttpMessageConverter);
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new FormHttpMessageConverter());
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("text", "plain", UTF8)));
messageConverters.add(stringHttpMessageConverter);
messageConverters.add(new BufferedImageHttpMessageConverter());
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
RestTemplate restTemplate = new RestTemplate();
restTemplate.setMessageConverters(messageConverters);
return restTemplate;
}
But it didn't change anything and the exception remains the same.
My understanding is that, it is not the REST template that needs any specific JSON configuration, but rather, that, for some reason, my controller is spitting out some application/octet-stream content type instead of some application/json content type.
Any clue?
Some additional information...
The admin rest client bean in the web test configuration:
#Configuration
public class WebTestConfiguration {
#Bean
public AdminRestClient adminRestClient() {
return new AdminRestClient();
}
#Bean
public RestTemplate restTemplate() {
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "json", UTF8)));
messageConverters.add(mappingJackson2HttpMessageConverter);
Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
jaxb2Marshaller.setClassesToBeBound(new Class[] {
Greeting.class
});
MarshallingHttpMessageConverter marshallingHttpMessageConverter = new MarshallingHttpMessageConverter(jaxb2Marshaller, jaxb2Marshaller);
messageConverters.add(marshallingHttpMessageConverter);
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new FormHttpMessageConverter());
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("text", "plain", UTF8)));
messageConverters.add(stringHttpMessageConverter);
messageConverters.add(new BufferedImageHttpMessageConverter());
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
RestTemplate restTemplate = new RestTemplate();
restTemplate.setMessageConverters(messageConverters);
return restTemplate;
}
}
The base test class:
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#ContextConfiguration( classes = { ApplicationConfiguration.class, WebSecurityConfig.class, WebConfiguration.class, WebTestConfiguration.class })
#Transactional
public abstract class AbstractControllerTest {
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private FilterChainProxy springSecurityFilterChain;
#Autowired
protected RestTemplate restTemplate;
protected MockRestServiceServer mockServer;
#Before
public void setup() {
this.mockServer = MockRestServiceServer.createServer(restTemplate);
}
}
The web init class:
public class WebInit implements WebApplicationInitializer {
private static Logger logger = LoggerFactory.getLogger(WebInit.class);
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
registerListener(servletContext);
registerDispatcherServlet(servletContext);
registerJspServlet(servletContext);
createSecurityFilter(servletContext);
}
private void registerListener(ServletContext servletContext) {
// Create the root application context
AnnotationConfigWebApplicationContext appContext = createContext(ApplicationConfiguration.class, WebSecurityConfig.class);
// Set the application display name
appContext.setDisplayName("LearnInTouch");
// Create the Spring Container shared by all servlets and filters
servletContext.addListener(new ContextLoaderListener(appContext));
}
private void registerDispatcherServlet(ServletContext servletContext) {
AnnotationConfigWebApplicationContext webApplicationContext = createContext(WebConfiguration.class);
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(webApplicationContext));
dispatcher.setLoadOnStartup(1);
Set<String> mappingConflicts = dispatcher.addMapping("/");
if (!mappingConflicts.isEmpty()) {
for (String mappingConflict : mappingConflicts) {
logger.error("Mapping conflict: " + mappingConflict);
}
throw new IllegalStateException(
"The servlet cannot be mapped to '/'");
}
}
private void registerJspServlet(ServletContext servletContext) {
}
private AnnotationConfigWebApplicationContext createContext(final Class... modules) {
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(modules);
return appContext;
}
private void createSecurityFilter(ServletContext servletContext) {
FilterRegistration.Dynamic springSecurityFilterChain = servletContext.addFilter("springSecurityFilterChain", DelegatingFilterProxy.class);
springSecurityFilterChain.addMappingForUrlPatterns(null, false, "/*");
}
}
The web configuration:
#Configuration
#EnableWebMvc
#EnableEntityLinks
#ComponentScan(basePackages = "com.thalasoft.learnintouch.rest.controller")
public class WebConfiguration extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
PageableArgumentResolver resolver = new PageableArgumentResolver();
resolver.setFallbackPageable(new PageRequest(1, 10));
resolvers.add(new ServletWebArgumentResolverAdapter(resolver));
super.addArgumentResolvers(resolvers);
}
}
The application configuration is empty for now:
#Configuration
#Import({ ApplicationContext.class })
public class ApplicationConfiguration extends WebMvcConfigurerAdapter {
// Declare "application" scope beans here, that is, beans that are not only used by the web context
}
I had my doubts before, but now that you've posted everything, here's what's up. Assuming the RestTemplate object you use in your getGreetingMessage() method is the same as the one declared in the #Bean method, the problem starts here
this.mockServer = MockRestServiceServer.createServer(restTemplate);
This call overwrites the default ClientHttpRequestFactory object that the RestTemplate object uses internally with a mock. In your getGreetingMessage() method, this call
ResponseEntity<GreetingResource> responseEntity = restTemplate.getForEntity("/admin/greeting", GreetingResource.class, httpHeaders);
doesn't actually go through the network. The RestTemplate uses the mocked ClientHttpRequestFactory to create a fake ClientHttpRequest which produces a fake ClientHttpResponse which doesn't have a Content-Type header. When the RestTemplate looks at the ClientHttpResponse to determine its Content-Type and doesn't find one, it assumes application/octet-stream by default.
So, your controller isn't setting the content type because your controller is never hit. The RestTemplate is using a default content type for your response because it is mocked and doesn't actually contain one.
From your comments:
I wonder if I understand what the mock server is testing. I understand
it is to be used in acceptance testing scenario. Is it supposed to hit
the controller at all ?
The javadoc for MockRestServiceServer states:
Main entry point for client-side REST testing. Used for tests that
involve direct or indirect (through client code)
use of the RestTemplate. Provides a way to set up fine-grained
expectations on the requests that will be performed through the
RestTemplate and a way to define the responses to send back removing
the need for an actual running server.
In other words, it's as if your application server didn't exist. So you could throw any expectations (and actual return values) you wanted and test whatever happens from the client side. So you aren't testing your server, you are testing your client.
Are you sure you aren't looking for MockMvc, which is
Main entry point for server-side Spring MVC test support.
which you can setup to actually use your #Controller beans in an integration environment. You aren't actually sending HTTP request, but the MockMvc is simulating how they would be sent and how your server would respond.
It is bug in MockHttpServletRequest and I will try to describe it.
Issue in tracker https://jira.springsource.org/browse/SPR-11308#comment-97327
Fixed in version 4.0.1
Bug
When DispatcherServlet looking for method to invoke it using some RequestConditions. One of them is ConsumesRequestCondition. The following is a piece of code:
#Override
protected boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeNotSupportedException {
try {
MediaType contentType = StringUtils.hasLength(request.getContentType()) ?
MediaType.parseMediaType(request.getContentType()) :
MediaType.APPLICATION_OCTET_STREAM;
return getMediaType().includes(contentType);
}
catch (IllegalArgumentException ex) {
throw new HttpMediaTypeNotSupportedException(
"Can't parse Content-Type [" + request.getContentType() + "]: " + ex.getMessage());
}
}
We are interested in piece request.getContentType(). There request is MockHttpServletRequest. Let's look on method getContentType():
public String getContentType() {
return this.contentType;
}
It just return value of this.contentType. It does not return a value from the header! And this.contentType is always NULL. Then contentType in matchMediaType methos will be always MediaType.APPLICATION_OCTET_STREAM.
Solution
I have tried many ways but have found only one that works.
Create package org.springframework.test.web.client in your test directory.
Create copy of org.springframework.test.web.client.MockMvcClientHttpRequestFactory but rename it. For example rename to FixedMockMvcClientHttpRequestFactory.
Find line:
MvcResult mvcResult = MockMvcClientHttpRequestFactory.this.mockMvc.perform(requestBuilder).andReturn();
Replace it with code:
MvcResult mvcResult = FixedMockMvcClientHttpRequestFactory.this.mockMvc.perform(new RequestBuilder() {
#Override
public MockHttpServletRequest buildRequest(ServletContext servletContext) {
MockHttpServletRequest request = requestBuilder.buildRequest(servletContext);
request.setContentType(request.getHeader("Content-Type"));
return request;
}
}).andReturn();
And register your ClientHttpReque
#Bean
public ClientHttpRequestFactory clientHttpRequestFactory(MockMvc mockMvc) {
return new FixedMockMvcClientHttpRequestFactory(mockMvc);
}
I know that it is not beautiful solution but it works fine.