Тестирование вместе с Spring Boot. Часть 1 | OTUS

Тестирование вместе с Spring Boot. Часть 1

Spring_Deep_16.11_site-5020-e91209.png

С появлением 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
    }
}
  1. Здесь Spring Boot c помощью @WebMvcTest (PersonController.class) создаёт фейковое окружение с настроенным Spring MVC и входящим в него Jackson, причём именно в том виде, в каком они настроены в реальном приложении.
  2. Далее мы создаём запрос с помощью mockMvc, кстати, его можно настраивать.
  3. А далее мы пишем набор matсher-ов, которые проверяют запрос, здесь можно проверить также и контент, и HTTP-заголовки.

Вот такими манипуляциями можно проверить и View-слой SpringBoot приложения. А как проверить DAO/Repository-слой, мы узнаем в следующей заметке.

Есть вопрос? Пишите комментарий!

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
3 комментария
4

Совет. Оставляйте код с импортами.

0

А как тестировать ExceptionHandler с @ControllerAdvice?

0

сложно и замысловато

Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто
Запланируй обучение с выгодой!
Получи скидку 10% на все курсы ноября и декабря до 17.11 →