Как сериализовать объект в application/hal+json в тесте Spring Boot Unit?

В проекте 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

Любая помощь будет принята с благодарностью!

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
0
126
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

в этом случае вы должны запустить тест вместе с контекстом 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);
}
}

со структурой, подобной этой, вы можете легко выполнять все утверждения и тесты

Спасибо за ваш ответ. Я уже использовал контекст Spring через @SpringBootTest так, как вы описали (сейчас обновил свой вопрос, включив в него эту часть), так что это не решает проблему. Что касается TestRestTemplate, я думаю, это может быть полезно при тестировании результатов запроса GET, например. г. (хотя я здесь предпочитаю Траверсона), но моя проблема связана с сериализацией объекта, который будет отправлен POST.

ahuemmer 15.03.2024 20: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 не инициализирует все правильно во время тестирования, как в обычном режиме выполнения.

Другие вопросы по теме