Search code examples
javaspring-bootunit-testingserializationspring-hateoas

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


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!


Solution

  • 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.