В проекте Spring Boot я пытаюсь сериализовать и десериализовать связанные объекты, используя HATEOAS в формате hal+json. Для этого я использую org.springframework.boot:spring-boot-starter-hateoas, и в моем контроллере он отлично работает:
@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);
}
Класс Item:
@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
}
Вызов метода GET приведет к чему-то вроде
{
"name": "foo"
"_links": {
"self": {
"href": "http://localhost:8080/items/1"
},
"category": {
"href": "http://localhost:8080/categories/2"
}
},
"item_id": 1
}
Это именно то, чего я хочу.
Проблема возникает сейчас, когда я пытаюсь написать тест для метода POST, который предназначен для создания новых Item. Вот как выглядит мой тест:
@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());
}
}
Эта сериализация не работает должным образом, поскольку в строке itemSerialized отношения выглядят следующим образом:
"links" : [{
"rel" : "category",
"href" : "http://localhost:61524/categories/2"
}]
(Правильно было бы _links с подчеркиванием и структурой отношений category, как показано выше.)
Насколько я понял, причина в том, что используемый мной objectMapper не знает о необходимости сериализации в формат json+hal. (Что правильно происходит по умолчанию/магии в контроллере, когда он возвращает Item из getItemById.) Теперь, каков был бы правильный способ гарантировать, что правильная сериализация происходит и в тесте?
Я читал об использовании objectMapper.registerModule(new Jackson2HalModule());, что мне кажется разумным. Но когда я его использую, возникает исключение:
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
Любая помощь будет принята с благодарностью!




в этом случае вы должны запустить тест вместе с контекстом Spring, чтобы платформа выполнила все шаги, которые необходимо выполнить внутри, чтобы вернуть правильный ответ HATEOAS.
также лучше использовать TestRestTemplate, поскольку это пружинное решение вместе с библиотеками jayway и AssertJ. Это облегчит тестирование, даже если вы используете TDD.
Ниже я оставляю пример, взятый из 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);
}
}
со структурой, подобной этой, вы можете легко выполнять все утверждения и тесты
FTR, я нашел решение после долгих поисков и попыток. В основном этот ответ привел меня на правильный путь. Главное — не только вызвать objectMapper.registerModule(new Jackson2HalModule());, но и предоставить HandlerInstantiator:
Тогда мой приведенный выше код будет изменен следующим образом: важная строка будет отмечена //!!.
@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);
// ...
Таким образом, ошибок не возникает, и результат выглядит так, как я хочу. В любом случае, я не знаю, является ли это лучшим возможным решением или почему Spring Hateoas не инициализирует все правильно во время тестирования, как в обычном режиме выполнения.
Спасибо за ваш ответ. Я уже использовал контекст Spring через
@SpringBootTestтак, как вы описали (сейчас обновил свой вопрос, включив в него эту часть), так что это не решает проблему. Что касается TestRestTemplate, я думаю, это может быть полезно при тестировании результатов запроса GET, например. г. (хотя я здесь предпочитаю Траверсона), но моя проблема связана с сериализацией объекта, который будет отправлен POST.