How to serialize object to application/hal+json in Spring Boot Unit test?

34 Views Asked by At

In a Spring Boot project, I am trying to serialize and deserialize related objects using HATEOAS in hal+json format. I'm using org.springframework.boot:spring-boot-starter-hateoas to do so and it works great in my controller:

@GetMapping(path = "/{id}", produces = MediaTypes.HAL_JSON_VALUE + ";charset=UTF-8")
HttpEntity<Item> getItemById(@PathVariable Integer id) {
    Item item = itemRepository.findById(id).get();
    item.add(linkTo(ItemController.class).slash(item.getId()).withSelfRel());
    item.add(linkTo(methodOn(CategoryController.class).getCategoryById(item.getCategory().getId()))
        .withRel("category"));
    return new HttpEntity<>(item);
}

With the Item class being:

@Entity
public class Item extends RepresentationModel<Item> {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "item_id")
    private Integer id;

    @ManyToOne
    @JoinColumn(name = "category_id", referencedColumnName = "category_id")
    @JsonIgnore
    private Category category;

    private String name;

    // Getters and Setters are present but not entered here in the question
}

Calling the GET method will result in something like

{
  "name": "foo"
  "_links": {
    "self": {
      "href": "http://localhost:8080/items/1"
    },
    "category": {
      "href": "http://localhost:8080/categories/2"
    }
  },
  "item_id": 1
}

This is just what I want.

The problem occurs now when I'm trying to write a test for the POST method which is intended to be used for creating new Items. This is what my test looks like:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@AutoConfigureMockMvc
public class ItemControllerTest{

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private MockMvc mockMvc;

    @LocalServerPort
    private int port;

    @Test
    void testCreate() throws Exception {
        Item testItem = new Item("test");
        testItem.add(Link.of("http://localhost:" + port + "/categories/2", "category"));

        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        ObjectWriter ow = objectMapper.writer().withDefaultPrettyPrinter();
        String itemSerialized = ow.writeValueAsString(testItem);

        mockMvc.perform(post("/items/")
            .content(itemSerialized)
            .contentType(MediaTypes.HAL_JSON_VALUE))
            .andExpect(status().isOK());
    }
}

This serialization does not work as needed, as the relations look like this in the itemSerialized String:

  "links" : [{
    "rel" : "category",
    "href" : "http://localhost:61524/categories/2"
  }]

(What would be correct is _links with the underscore and a category relation structure like given above.)

As I understood, the cause of this is, that the objectMapper I use is not aware of the need to serialize to json+hal format. (Which correctly happens by default/magic in the Controller when it returns the Item from getItemById.) Now, what would be the proper way to ensure, that the correct serialization happens in the test as well?

I have read about using objectMapper.registerModule(new Jackson2HalModule());, which seems reasonable to me. But when I use it, an exception occurs:

org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'org.springframework.hateoas.mediatype.hal.Jackson2HalModule$HalLinkListSerializer': 
Failed to instantiate [org.springframework.hateoas.mediatype.hal.Jackson2HalModule$HalLinkListSerializer]: 
No default constructor found

Any help would be greatly appreciated!

2

There are 2 best solutions below

0
ahuemmer On BEST ANSWER

FTR, I found a solution after searching and trying a lot. Mainly this answer led me the right way. The key is not only to call objectMapper.registerModule(new Jackson2HalModule());, but also to supply a HandlerInstantiator:

My code from above would then be amended like this, the crucial line marked with //!!.

@Autowired 
private LinkRelationProvider linkRelationProvider;

@Autowired 
private MessageResolver messageResolver

// ...

    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    objectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(
        linkRelationProvider, CurieProvider.NONE, messageResolver)); //!!
    ObjectWriter ow = objectMapper.writer().withDefaultPrettyPrinter();
    String itemSerialized = ow.writeValueAsString(testItem);

// ...

This way, no error occurs and the result looks like I want it to. Anyway, I don't know if this is the best possible solution or why Spring Hateoas doesn't initialize things correctly at test time as it does at normal runtime.

1
Filipe Cavalcanti On

in this case you must run your test together with the spring context so that the framework performs all the steps that must be done internally to return a correct HATEOAS response.

it is also better to use TestRestTemplate as it is a spring solution together with jayway and AssertJ libs. This will make testing easier, even if you are doing TDD

Below I leave an example taken from Spring Academy

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CashCardApplicationTests {
@Autowired
TestRestTemplate restTemplate;

@Test
void shouldReturnACashCardWhenDataIsSaved() {
    ResponseEntity<String> response = restTemplate.getForEntity("/cashcards/99", String.class);
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

    DocumentContext documentContext = JsonPath.parse(response.getBody());
    Number id = documentContext.read("$.id");
    assertThat(id).isEqualTo(99);

    Double amount = documentContext.read("$.amount");
    assertThat(amount).isEqualTo(123.45);
}
}

with a structure similar to this you can perform all assertions and tests with ease