Spring Boot's TestRestTemplate with HATEOAS Pa

2019-06-28 05:07发布

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?

2条回答
Evening l夕情丶
2楼-- · 2019-06-28 05:46

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.

查看更多
乱世女痞
3楼-- · 2019-06-28 05:48

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>
查看更多
登录 后发表回答