HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>Теги: spring, spring framework, personrepository, json, ajax, jackson, spring boot, spring security, persondto, доменный класс person, аннотация @jsontest, аннотация @webmvctest, personcontroller.class, mockmvc, matсher, dao/repository</p>
1 <p>Теги: spring, spring framework, personrepository, json, ajax, jackson, spring boot, spring security, persondto, доменный класс person, аннотация @jsontest, аннотация @webmvctest, personcontroller.class, mockmvc, matсher, dao/repository</p>
2 <p>С появлением<strong>Spring Boot</strong>появилась масса возможностей для тестирования различных слоёв. Итак, у нас есть приложение c backend и UI. UI использует backend, а backend содержит следующий код:</p>
2 <p>С появлением<strong>Spring Boot</strong>появилась масса возможностей для тестирования различных слоёв. Итак, у нас есть приложение c backend и UI. UI использует backend, а backend содержит следующий код:</p>
3 // 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)); } }<p>Данный код содержит<strong>REST контроллер</strong>, который получает и изменяет объекты Person, используя<strong>PersonRepository</strong>. Этот контроллер возвращает/принимает JSON и он будет использовать UI приложения с помощью<strong>AJAX</strong>.</p>
3 // 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)); } }<p>Данный код содержит<strong>REST контроллер</strong>, который получает и изменяет объекты Person, используя<strong>PersonRepository</strong>. Этот контроллер возвращает/принимает JSON и он будет использовать UI приложения с помощью<strong>AJAX</strong>.</p>
4 <p>Также присутствует специальный класс<strong>PersonDto</strong>, который будет маппиться в JSON. Этот класс нужен для того, чтобы зафиксировать формат JSON и можно было править доменный класс<strong>Person</strong>независимо и не изменять формат<strong>JSON</strong>, который используется на UI приложения.</p>
4 <p>Также присутствует специальный класс<strong>PersonDto</strong>, который будет маппиться в JSON. Этот класс нужен для того, чтобы зафиксировать формат JSON и можно было править доменный класс<strong>Person</strong>независимо и не изменять формат<strong>JSON</strong>, который используется на UI приложения.</p>
5 <p>В нашем коде не предусмотрено никакого<strong>Spring Security</strong>и методы должны возвращаться вне зависимости от аутентификации и авторизации.</p>
5 <p>В нашем коде не предусмотрено никакого<strong>Spring Security</strong>и методы должны возвращаться вне зависимости от аутентификации и авторизации.</p>
6 <h2>Что же мы хотим протестировать и для чего?</h2>
6 <h2>Что же мы хотим протестировать и для чего?</h2>
7 <p>Есть несколько кейсов, где тесты действительно нужны, итак: 1. Контроллер отправляет и принимает<strong>PersonDto</strong>, а работает с доменными сущностями<strong>Person</strong>. Эти преобразования осуществляются с помощью статических методов<strong>PersonDto</strong>и должны быть покрыты тестами просто по правилам приличия. 2. Конкретный<strong>PersonDto</strong>преобразуется в JSON в методе получения и используется на UI. Нам хочется зафиксировать формат JSON, который будет возвращаться, чтобы при исправлениях в backend приложения мы не изменили этот формат и не поломали UI. 3. JSON с данными о<strong>Person</strong>также отправляется c UI на backend, плюс хочется также проверить, что отправляемые сейчас данные будут корректно приниматься backend-ом. 4. Контроллер по идее возвращает 200 OK всегда при существующих данных, поэтому хочется зафиксировать данное поведение при каждом запросе.</p>
7 <p>Есть несколько кейсов, где тесты действительно нужны, итак: 1. Контроллер отправляет и принимает<strong>PersonDto</strong>, а работает с доменными сущностями<strong>Person</strong>. Эти преобразования осуществляются с помощью статических методов<strong>PersonDto</strong>и должны быть покрыты тестами просто по правилам приличия. 2. Конкретный<strong>PersonDto</strong>преобразуется в JSON в методе получения и используется на UI. Нам хочется зафиксировать формат JSON, который будет возвращаться, чтобы при исправлениях в backend приложения мы не изменили этот формат и не поломали UI. 3. JSON с данными о<strong>Person</strong>также отправляется c UI на backend, плюс хочется также проверить, что отправляемые сейчас данные будут корректно приниматься backend-ом. 4. Контроллер по идее возвращает 200 OK всегда при существующих данных, поэтому хочется зафиксировать данное поведение при каждом запросе.</p>
8 <h2>Поехали?</h2>
8 <h2>Поехали?</h2>
9 <p>Для тестирования статических методов, никакого<strong>SpringBoot</strong>и не надо!</p>
9 <p>Для тестирования статических методов, никакого<strong>SpringBoot</strong>и не надо!</p>
10 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()); } }<p>Зафиксируем теперь формат<strong>JSON</strong>, в который превращается DTO. Вот здесь и помогает SpringBoot - в данном случае аннотация<strong>@JsonTest</strong>инициализирует контекст и включает<strong>Jackson</strong>в той конфигурации, что используется в проекте.</p>
10 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()); } }<p>Зафиксируем теперь формат<strong>JSON</strong>, в который превращается DTO. Вот здесь и помогает SpringBoot - в данном случае аннотация<strong>@JsonTest</strong>инициализирует контекст и включает<strong>Jackson</strong>в той конфигурации, что используется в проекте.</p>
11 @JsonTest class PersonDtoTest { // старые методы, конечно остаются @Autowired private JacksonTester&lt;PersonDto&gt; 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"); } }<p>А вот теперь пришла пора проверить, как десериализуется<strong>JSON</strong>. В случае сложных DTO и различных по виду запросов стоит такие фикстуры создать для каждого более-менее отличного запроса, желательно реального, взятого из браузера или из логов.</p>
11 @JsonTest class PersonDtoTest { // старые методы, конечно остаются @Autowired private JacksonTester&lt;PersonDto&gt; 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"); } }<p>А вот теперь пришла пора проверить, как десериализуется<strong>JSON</strong>. В случае сложных DTO и различных по виду запросов стоит такие фикстуры создать для каждого более-менее отличного запроса, желательно реального, взятого из браузера или из логов.</p>
12 @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()); } }<p>А теперь осталось написать Unit-тест на контроллер. На самом деле не так просто определить зону ответственности контроллера, т. е. какой функционал покрыть тестами. Основная задача контроллера - это иметь дело с HTTP-запросами и ответами и правильно вызывать бизнес-метод. Именно это и протестируем, оставив формат зафиксированным в тесте на DTO.</p>
12 @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()); } }<p>А теперь осталось написать Unit-тест на контроллер. На самом деле не так просто определить зону ответственности контроллера, т. е. какой функционал покрыть тестами. Основная задача контроллера - это иметь дело с HTTP-запросами и ответами и правильно вызывать бизнес-метод. Именно это и протестируем, оставив формат зафиксированным в тесте на DTO.</p>
13 <p>В<strong>Spring Boot</strong>имеется аннотация<strong>@WebMvcTest</strong>, которая позволяет писать как раз такие unit-тесты:</p>
13 <p>В<strong>Spring Boot</strong>имеется аннотация<strong>@WebMvcTest</strong>, которая позволяет писать как раз такие unit-тесты:</p>
14 @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 } }<ol><li>Здесь Spring Boot c помощью<strong>@WebMvcTest</strong>(PersonController.class) создаёт фейковое окружение с настроенным<strong>Spring MVC</strong>и входящим в него<strong>Jackson</strong>, причём именно в том виде, в каком они настроены в реальном приложении.</li>
14 @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 } }<ol><li>Здесь Spring Boot c помощью<strong>@WebMvcTest</strong>(PersonController.class) создаёт фейковое окружение с настроенным<strong>Spring MVC</strong>и входящим в него<strong>Jackson</strong>, причём именно в том виде, в каком они настроены в реальном приложении.</li>
15 <li>Далее мы создаём запрос с помощью<strong>mockMvc</strong>, кстати, его можно настраивать.</li>
15 <li>Далее мы создаём запрос с помощью<strong>mockMvc</strong>, кстати, его можно настраивать.</li>
16 <li>А далее мы пишем набор<strong>matсher</strong>-ов, которые проверяют запрос, здесь можно проверить также и контент, и HTTP-заголовки.</li>
16 <li>А далее мы пишем набор<strong>matсher</strong>-ов, которые проверяют запрос, здесь можно проверить также и контент, и HTTP-заголовки.</li>
17 </ol><p>Вот такими манипуляциями можно проверить и<strong>View</strong>-слой SpringBoot приложения. А как проверить<strong>DAO/Repository</strong>-слой, мы узнаем в следующей заметке.</p>
17 </ol><p>Вот такими манипуляциями можно проверить и<strong>View</strong>-слой SpringBoot приложения. А как проверить<strong>DAO/Repository</strong>-слой, мы узнаем в следующей заметке.</p>
18 <p><em>Есть вопрос? Пишите комментарий!</em></p>
18 <p><em>Есть вопрос? Пишите комментарий!</em></p>
19  
19