I'm trying to use the TestRestTemplate in my Spring Boot Application's Integration-Test, to make a request to a Spring Data REST Repository.
The response in the browser has the form:
{
"links": [
{
"rel": "self",
"href": "http://localhost:8080/apiv1/data/users"
},
{
"rel": "profile",
"href": "http://localhost:8080/apiv1/data/profile/users"
},
{
"rel": "search",
"href": "http://localhost:8080/apiv1/data/users/search"
}
],
"content": [
{
"username": "admin",
"enabled": true,
"firstName": null,
"lastName": null,
"permissions": [ ],
"authorities": [
"ROLE_ADMIN"
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"content": [ ],
"links": [
{
"rel": "self",
"href": "http://localhost:8080/apiv1/data/users/1"
},
{
"rel": "myUser",
"href": "http://localhost:8080/apiv1/data/users/1"
},
{
"rel": "mandant",
"href": "http://localhost:8080/apiv1/data/users/1/mandant"
}
]
},
{
"username": "dba",
"enabled": true,
"firstName": null,
"lastName": null,
"permissions": [ ],
"authorities": [
"ROLE_DBA"
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"content": [ ],
"links": [
{
"rel": "self",
"href": "http://localhost:8080/apiv1/data/users/2"
},
{
"rel": "myUser",
"href": "http://localhost:8080/apiv1/data/users/2"
},
{
"rel": "mandant",
"href": "http://localhost:8080/apiv1/data/users/2/mandant"
}
]
},
{
"username": "user",
"enabled": true,
"firstName": null,
"lastName": null,
"permissions": [ ],
"authorities": [
"ROLE_USER"
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"content": [ ],
"links": [
{
"rel": "self",
"href": "http://localhost:8080/apiv1/data/users/3"
},
{
"rel": "myUser",
"href": "http://localhost:8080/apiv1/data/users/3"
},
{
"rel": "mandant",
"href": "http://localhost:8080/apiv1/data/users/3/mandant"
}
]
}
],
"page": {
"size": 20,
"totalElements": 3,
"totalPages": 1,
"number": 0
}
}
This is the test:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("unittest")
public class MyUserRepositoryIntegrationTest {
private static Logger logger = LoggerFactory.getLogger(MyUserRepositoryIntegrationTest.class);
private static final int NUM_USERS = 4;
private static final String USER_URL = "/apiv1/data/users";
@Autowired
private TestRestTemplate restTemplate;
@Test
public void listUsers() {
ResponseEntity<PagedResources<MyUser>> response = restTemplate.withBasicAuth("user", "user").exchange(USER_URL,
HttpMethod.GET, null, new ParameterizedTypeReference<PagedResources<MyUser>>() {
});
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
logger.debug("Res : " + response.getBody().toString());
assertThat(response.getBody().getContent().size()).isEqualTo(NUM_USERS);
}
@TestConfiguration
public static class MyTestConfig {
@Autowired
@Qualifier("halJacksonHttpMessageConverter")
private TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter;
@Bean
public RestTemplateBuilder restTemplateBuilder() {
return new RestTemplateBuilder().messageConverters(halJacksonHttpMessageConverter);
}
}
}
The Problem is, that I don't get the content. Interestingly, the Metadata (paging-info) is there.
My TestConfig gets detected, but I think it's not using the 'halJacksonHttpMessageConverter' (which I got from here: https://github.com/bijukunjummen/hateoas-sample/blob/master/src/test/java/univ/HALRestTemplateIntegrationTests.java). That's why I used "messageConverters()" and not "additionalMessageConverters()" (to no avail).
Here's the log:
m.m.a.RequestResponseBodyMethodProcessor : Written [PagedResource { content: [Resource { content: at.mycompany.myapp.auth.MyUser@7773211c, links: [<http://localhost:51708/apiv1/data/users/1>;rel="self", <http://localhost:51708/apiv1/data/users/1>;rel="logisUser"] }, Resource { content: at.mycompany.myapp.auth.MyUser@2c96fdee, links: [<http://localhost:51708/apiv1/data/users/2>;rel="self", <http://localhost:51708/apiv1/data/users/2>;rel="logisUser"] }, Resource { content: at.mycompany.myapp.auth.MyUser@1ddfd104, links: [<http://localhost:51708/apiv1/data/users/3>;rel="self", <http://localhost:51708/apiv1/data/users/3>;rel="logisUser"] }, Resource { content: at.mycompany.myapp.auth.MyUser@55d71419, links: [<http://localhost:51708/apiv1/data/users/4>;rel="self", <http://localhost:51708/apiv1/data/users/4>;rel="logisUser"] }], metadata: Metadata { number: 0, total pages: 1, total elements: 4, size: 20 }, links: [<http://localhost:51708/apiv1/data/users>;rel="self", <http://localhost:51708/apiv1/data/profile/users>;rel="profile", <http://localhost:51708/apiv1/data/users/search>;rel="search"] }] as "application/hal+json" using [org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration$ResourceSupportHttpMessageConverter@2f58f492]
o.s.web.servlet.DispatcherServlet : Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapter completed request handling
o.s.web.client.RestTemplate : GET request for "http://localhost:51708/apiv1/data/users" resulted in 200 (null)
o.s.web.servlet.DispatcherServlet : Successfully completed request
o.s.web.client.RestTemplate : Reading [org.springframework.hateoas.PagedResources<at.mycompany.myapp.auth.MyUser>] as "application/hal+json;charset=UTF-8" using [org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration$ResourceSupportHttpMessageConverter@10ad95cd]
o.s.b.w.f.OrderedRequestContextFilter : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@76e257e2
d.l.a.MyUserRepositoryIntegrationTest : Res : PagedResource { content: [], metadata: Metadata { number: 0, total pages: 1, total elements: 4, size: 20 }, links: [] }
The idea of overriding the restTemplate Bean comes from the docs: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html#boot-features-rest-templates-test-utility
Any Ideas how I can simply make some REST calls and get the answer as an Object for my tests?
I did a similar test, but I'm not using spring-boot. Probably is the configuration of your RestTemplate. By the way, have you tried to use the Traverson
implementation rather than RestTemplate
? It's seems more simple to work with HATEOAS. See bellow my test class with both approaches.
package org.wisecoding.api;
import org.junit.Test;
import org.wisecoding.api.domain.User;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.client.Traverson;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import static org.springframework.hateoas.client.Hop.rel;
public class UserApiTest {
@Test
public void testGetUsersRestTemplate() {
final ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new Jackson2HalModule());
final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(MediaType.parseMediaTypes(MediaTypes.HAL_JSON_VALUE));
converter.setObjectMapper(mapper);
final List<HttpMessageConverter<?>> list = new ArrayList<HttpMessageConverter<?>>();
list.add(converter);
final RestTemplate restTemplate = new RestTemplate(list);
final String authorsUrl = "http://localhost:8080/apiv1/users";
final ResponseEntity<PagedResources<User>> responseEntity = restTemplate.exchange(authorsUrl, HttpMethod.GET, null, new ParameterizedTypeReference<PagedResources<User>>() {});
final PagedResources<User> resources = responseEntity.getBody();
final List<User> users = new ArrayList(resources.getContent());
}
@Test
public void testGetUsersTraverson() throws Exception {
final Traverson traverson = new Traverson(new URI("http://localhost:8080/apiv1"), MediaTypes.HAL_JSON);
final ParameterizedTypeReference<PagedResources<User>> resourceParameterizedTypeReference = new ParameterizedTypeReference<PagedResources<User>>() {};
final PagedResources<User> resources = traverson.follow(rel("users")).toObject(resourceParameterizedTypeReference);
final List<User> users = new ArrayList(resources.getContent());
}
}
And also, the pom.xml
in case your dependencies does not match:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>war</packaging>
<groupId>org.wisecoding</groupId>
<version>0.1-SNAPSHOT</version>
<artifactId>user-demo-data-rest</artifactId>
<properties>
<spring.version>4.2.6.RELEASE</spring.version>
<slf4j.version>1.7.1</slf4j.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.0.4.v20130625</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-json-org</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-webmvc</artifactId>
<version>2.5.6.RELEASE</version>
<exclusions>
<exclusion>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.10.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.0.13</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.2.3.Final</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>1.8.0.10</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>central</id>
<url>http://central.maven.org/maven2/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
I switched to MockMvc and everything works:
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("unittest")
public class MyUserRepositoryIntegrationTest {
@Autowired WebApplicationContext context;
@Autowired FilterChainProxy filterChain;
MockMvc mvc;
@Before
public void setupTests() {
this.mvc = MockMvcBuilders.webAppContextSetup(context).addFilters(filterChain).build();
@Test
public void listUsers() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.ACCEPT, MediaTypes.HAL_JSON_VALUE);
headers.add(HttpHeaders.AUTHORIZATION, "Basic " + new String(Base64.encode(("user:user").getBytes())));
mvc.perform(get(USER_URL).headers(headers))
.andExpect(content().contentTypeCompatibleWith(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(NUM_USERS)));
}
}
EDIT:
For those interested, an alternative solution based on Wellington Souza's solution:
While jsonpath is really powerful, I haven't found a way to really unmarshall the JSON into an actual Object with MockMvc.
If you look at my posted JSON output, you'll notice, that it's not the default Spring Data Rest HAL+JSON output. I changed the property data.rest.defaultMediaType to "application/json". With that, I couldn't get Traverson to work either. But when I deactivate that, the following works:
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.client.Hop;
import org.springframework.hateoas.client.Traverson;
import org.springframework.http.HttpHeaders;
import org.springframework.security.crypto.codec.Base64;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("unittest")
public class MyUserRepositoryIntegrationTest {
private static HttpHeaders userHeaders;
private static HttpHeaders adminHeaders;
@LocalServerPort
private int port;
@BeforeClass
public static void setupTests() {
MyUserRepositoryIntegrationTest.userHeaders = new HttpHeaders();
MyUserRepositoryIntegrationTest.userHeaders.add(HttpHeaders.ACCEPT, MediaTypes.HAL_JSON_VALUE);
MyUserRepositoryIntegrationTest.userHeaders.add(HttpHeaders.AUTHORIZATION,
"Basic " + new String(Base64.encode(("user:user").getBytes())));
MyUserRepositoryIntegrationTest.adminHeaders = new HttpHeaders();
MyUserRepositoryIntegrationTest.adminHeaders.add(HttpHeaders.ACCEPT, MediaTypes.HAL_JSON_VALUE);
MyUserRepositoryIntegrationTest.adminHeaders.add(HttpHeaders.AUTHORIZATION,
"Basic " + new String(Base64.encode(("admin:admin").getBytes())));
}
@Test
public void listUsersSorted() throws Exception {
final ParameterizedTypeReference<PagedResources<MyUser>> resourceParameterizedTypeReference = //
new ParameterizedTypeReference<PagedResources<MyUser>>() {
};
final PagedResources<MyUser> actual = new Traverson(new URI("http://localhost:" + port + "/apiv1/data"),
MediaTypes.HAL_JSON)//
.follow(Hop.rel("myUsers").withParameter("sort", "username,asc"))//
.withHeaders(userHeaders)//
.toObject(resourceParameterizedTypeReference);
assertThat(actual.getContent()).isNotNull().isNotEmpty();
assertThat(actual.getContent()//
.stream()//
.map(user -> user.getUsername())//
.collect(Collectors.toList())//
).isSorted();
}
}
(Note: Might not contain all imports etc., since I copied this from a larger Test Class.)
The ".withParam" works for templated URLs, i.e., ones that accept query parameters. If you try to follow the raw URL, it will fail, because the link is literally "http://[...]/users{option1,option2,...}" and thus not well-formed.