Тестирование вместе с Spring Boot. Часть 1
С появлением Spring Boot появилась масса возможностей для тестирования различных слоёв. Итак, у нас есть приложение c backend и UI. UI использует backend, а backend содержит следующий код:
// DTO для Jackson public class PersonDto { private int id; private String name; // No args constructor, getters, setters public static PersonDto toDto(Person person) { return new PersonDto() {{ setId(person.getId()); setName(person.getName()); }}; } public Person toDomain() { return new Person(id, name); } } // PersonController @RestController public class PersonController { private final PersonRepository repository; // constructor @GetMapping("/person/{id}") public PersonDto getById(@PathVariable("id") int id) { return PersonDto.toDto(personRepository.getById(id)); } @PutMapping("/person") public void update(@RequestBody PersonDto personDto) { personRepository.update(PersonDto.toDomain(personDto)); } }
Данный код содержит REST контроллер, который получает и изменяет объекты Person, используя PersonRepository. Этот контроллер возвращает/принимает JSON и он будет использовать UI приложения с помощью AJAX.
Также присутствует специальный класс PersonDto, который будет маппиться в JSON. Этот класс нужен для того, чтобы зафиксировать формат JSON и можно было править доменный класс Person независимо и не изменять формат JSON, который используется на UI приложения.
В нашем коде не предусмотрено никакого Spring Security и методы должны возвращаться вне зависимости от аутентификации и авторизации.
Что же мы хотим протестировать и для чего?
Есть несколько кейсов, где тесты действительно нужны, итак: 1. Контроллер отправляет и принимает PersonDto, а работает с доменными сущностями Person. Эти преобразования осуществляются с помощью статических методов PersonDto и должны быть покрыты тестами просто по правилам приличия. 2. Конкретный PersonDto преобразуется в JSON в методе получения и используется на UI. Нам хочется зафиксировать формат JSON, который будет возвращаться, чтобы при исправлениях в backend приложения мы не изменили этот формат и не поломали UI. 3. JSON с данными о Person также отправляется c UI на backend, плюс хочется также проверить, что отправляемые сейчас данные будут корректно приниматься backend-ом. 4. Контроллер по идее возвращает 200 OK всегда при существующих данных, поэтому хочется зафиксировать данное поведение при каждом запросе.
Поехали?
Для тестирования статических методов, никакого SpringBoot и не надо!
public class PersonDtoTest { @Test public void testToDto() { PersonDto dto = PersonDto.toDto(new Person(42, "Ivan")); assertEquals(42, dto.getId()); assertEqulas("Ivan", dto.getName()); } @Test public void testToDomain() { Person domain = PersonDto.toDomain(new PersonDto() {{ setId(42); setName("Ivan"); }}; assertEquals(42, domain.getId()); assertEqulas("Ivan", domain.getName()); } }
Зафиксируем теперь формат JSON, в который превращается DTO. Вот здесь и помогает SpringBoot — в данном случае аннотация @JsonTest инициализирует контекст и включает Jackson в той конфигурации, что используется в проекте.
@JsonTest class PersonDtoTest { // старые методы, конечно остаются @Autowired private JacksonTester<PersonDto> json; @Test void testSerializePerson() throws Exception { Person domain = PersonDto.toDomain(new PersonDto() {{ setId(42); setName("Ivan"); }}; assertThat(this.json.write(dto)) .isStrictlyEqualToJson("simple-person.json"); } }
А вот теперь пришла пора проверить, как десериализуется JSON. В случае сложных DTO и различных по виду запросов стоит такие фикстуры создать для каждого более-менее отличного запроса, желательно реального, взятого из браузера или из логов.
@JsonTest class PersonDtoTest { // старые методы, конечно остаются @Test void testDeserializePerson() throws Exception { PersonDto dto = this.json.read("person-john-smith.json").getObject(); assertEquals(123, dto.getId()); assertEquals("John smith", dto.getId()); } }
А теперь осталось написать Unit-тест на контроллер. На самом деле не так просто определить зону ответственности контроллера, т. е. какой функционал покрыть тестами. Основная задача контроллера — это иметь дело с HTTP-запросами и ответами и правильно вызывать бизнес-метод. Именно это и протестируем, оставив формат зафиксированным в тесте на DTO.
В Spring Boot имеется аннотация @WebMvcTest, которая позволяет писать как раз такие unit-тесты:
@RunWith(SpringRunner.class) @WebMvcTest(PersonController.class) public class PersonControllerTest { @MockBean private PersonRepository repository; @Autowired private MockMvc mockMvc; @Test public void testReturn200() throws Exception { given(repository.getById(any())).willReturn(new Person(42, "Ivan")); mockMvc.perform(get("/person/42") .andExpect(status().isOk()) .andExpect(content() .contentTypeCompatibleWith(MediaType.APPLICATION_JSON)); varify } }
- Здесь Spring Boot c помощью @WebMvcTest (PersonController.class) создаёт фейковое окружение с настроенным Spring MVC и входящим в него Jackson, причём именно в том виде, в каком они настроены в реальном приложении.
- Далее мы создаём запрос с помощью mockMvc, кстати, его можно настраивать.
- А далее мы пишем набор matсher-ов, которые проверяют запрос, здесь можно проверить также и контент, и HTTP-заголовки.
Вот такими манипуляциями можно проверить и View-слой SpringBoot приложения. А как проверить DAO/Repository-слой, мы узнаем в следующей заметке.
Есть вопрос? Пишите комментарий!