Открытый проект BootJava.

Программа. Материалы проекта

GitHub (см. 1.3 настройки проекта)

Документирование. Тестирование. Кэширование

6.1 Документирование REST API: Swagger / OpenAPI 3.0

Современных подход к разработке программного обеспечения: автоматизировать все что можно. Почитайте про опыт Yandex: Использование Swagger/OpenAPI Specification (по крайней мере до деталей реализации). Для больших проектов это значительная экономия времени-ресурсов + устранение ошибок рассинхронизации API, клиента, тестов и документации. Есть несколько подходов автоматического генерирования документации, например Spring REST Docs — генерирование документации на основе тестов. Подключать его сложнее и, на мой взгляд, он менее распространен, чем генерация документации на основе Spring контроллеров через OpenAPI/Swagger.

Есть 2 способа подключения OpenAPI 3.0 в Spring Boot проект: через Springdoc-openapi и Springfox (см. также migrating from springfox swagger2 to springdoc openapi). Я выбрал первый:

  • Для авторизации и поддержки Spring Data REST, кроме springdoc-openapi-ui добавил зависимости springdoc-openapi-security и springdoc-openapi-data-rest.
  • Добавил конфигурацию OpenApi в классе OpenApiConfig (OpenAPI/Swagger Basic Auth authorization)
  • Для отображения имени контроллера добавил аннотацию @Tag в AccountController контроллер и UserRepository, на основе которого Spring Data REST генерирует User Controller (Swagger-2.X аннтоции)
После патча запуститесь и можно тестировать REST API нашего приложения (не забудте предварительно атворизоваться через Authorize справа вверху). С помощью плагинов Maven также можно генерировать документацию на этапе сборки и автогенерировать код для API.
Apply patch 6_01_oas3_swagger

Обновление и исправления

  • Обновил версию Spring Boot на 2.4.4 (откатите, если у вас новее)
  • Добавил ломбуковский @UtilityClass в утильные классы для приватных конструкторов и удалил лишний @AllArgsConstructor
  • Добавил в Exception Handler логирование (по умолчанию стандартные ошибки возвращаются без сообщения, чтобы не раскрывать детали реализации)
  • Поменял ddl-auto. У нас inmemory база и drop приводит к эксепшенам в логах тестов
  • Починил consumes = MediaType.APPLICATION_JSON_VALUE в AccountController
Apply patch 6_02_fix_update
Внимание: если обновились на версию spring-boot 2.5.0 и выше, добавьте в application.yaml:
spring.jpa.defer-datasource-initialization: true

6.2 Тестирование

Вариантов тестирования Spring Boot приложения большое множество. Можно поднимать не весь контекст приложения, а только его часть, например только слой репозитория, см Тестовые срезы Spring Boot. Можно, наоборот, поднимать только выбранный контроллер, а остальное мокать. Наконец, можно поднимать весь контекст и тестировать через MockMvc, WebTestClient или RestTemplate. Кроме того, популярно тестирование с помощью REST-Assured (Testing Spring Boot with REST-Assured). Если теперь это умножить на варианты проверки результатов, даже через стандартные библиотеки, которые подтягивает spring-boot-starter-test, мы получим огромное количество информации по запросу тестирование Spring Boot.

На стажировке TopJava мы тестируем как сервисы, так и контроллеры (более 130 тестов). В тестовом приложении на работу (мое личное мнение) не стоит стараться покрыть тестами 100% функционала. Достаточно показать ваш подход к тестированию и сделать тесты самых важных сценариев (юзкейсов). Для тестирования контроллеров в TopJava мы использовали MockMvc, который приходилось самостоятельно настраивать. Spring Boot аннотации @AutoConfigureMockMvc и @SpringBootTest делают это за нас, остается только заинжектить его в тесты, см. базовый класс для всех тестов контроллеров AbstractControllerTest. Добавим зависимость spring-security-test и имитацию аутентификации через @WithUserDetails (см. mock authentication in Spring) и мы уже можем писать тесты к нашим контроллерам.

Apply patch 6_03_add_tests
mvn clean test

Теперь подключим поддержку JSON: нужно тестировать запросы с телом (create/update) и проверять содержимое ответов. Вручную писать JSON строки неинтересно, сделаем класс для сериализации-десериализации: JsonUtil. Я разместил его в классах приложения — достаточно часто приходится работать с JSON не только в тестах, но и самом приложении и сделал класс утилитным (обратите внимание, что я не создаю ObjectMapper, а беру из WebSecurityConfig Spring-овый, со всеми настройками). В класс UserTestUtil с тестовыми данными добавим методы для получения созданного и обновленного объекта User и используем их в тестах create/update.

Apply patch 6_04_json_support
mvn test

И последнее — проверка тела ответов и объектов в базе после create/update. Создаем в UserTestUtil эталонные объекты для сравнения (те же, что мы вставляем в базу через data.sql). Мы не можем сравнивать entity-объекты, переопределяя equals по всем полям (очень частая ошибка): how should equals and hashcode be implemented when using JPA and Hibernate. В реальных проектах обычно объекты сравниваются по PK (обычно сравнение происходит уже после сохранения в базе, см. реализацию AbstractPersistable от Spring Data JPA). А для сравнения по всем полям (исключая закодированный password) удобно использовать библиотеку AssertJ, которая транзитивно подтягивается с spring-boot-starter-test: Field by field recursive comparison. Напомню, что по идеологии HATEOAS id в ответах не отдается, поэтому для тестирования UserControllerTest сделал еще один метод проверки UserTestUtil#assertNoIdEquals. Работа с _links вместо id и проверка в UserControllerTest#getAll содержимого ответа становится не совсем тривиальной задачей. Если решитесь работать с HATEOAS, предлагаю решить ее вам самим.

Apply patch 6_05_test_body_check

6.3 Кэширование

На 5-м занятии стажировки TopJava мы добавляем к проекту Spring кэш на основе Ehcache 3, на 6-м — кэш Hibernate 2-го уровня. Про основы кэша можно посмотреть начало открытого видео TopJava. А на нашем проекте мы используем для Spring кэша простую и эффективную реализацию в памяти на основе переписанной части библиотеки Guava: Caffeine Cache. Еще раз подчеркну: кэшируется то, что часто запрашивается и редко меняется. При базовой авторизации обращение в базу идет при каждом запросе (эту проблему решает JWT аутентификация, но она реализуется сложнее), поэтому можно закэшировать результат запроса в ДБ при аутентификации: UserRepository#findByEmailIgnoreCase. Добавим над методом соответствующую аннотацию.

Основное, что следует помнить при добавлении кэша — его инвалидация, те очистка, когда данные становятся неверными. В AccountController это сделать несложно: расставляем аннотации над методами, которые меняют пользователя (обратите внимание, что я при update сделал return для @CachePut, а в аннотациях использую Custom Key Generation). Проверить работу кэша можно по логам обращения к базе:

Apply patch 6_06_add_cache

При изменения пользователей админом кэш также следует инвалидировать: добавил аннотации кэширования в UserRepository. Здесь следует быть осторожным, потому что мы не учли все возможные случаи в репозитории (saveAll, saveAndFlush, deleteInBatch, ...), а также при удалении по id инвалидируется весь кэш.

Наконец последнее - кэш часто мешает тестам и его надо чистить перед каждым тестом. Или, как мы сделали на TopJava18, вообще отключать кэш в тестах: запускаем тесты с профилем @ActiveProfiles("test") и отключаем его в application-test.yaml, см. Profile Specific Files.

Вообще кэширование рекомендуется делать в сервисах, что исключает автогенерацию Spring Data REST.
И обычно от него много проблем, поэтому в тестовом приложении не делайте кэши "на всякий случай", только очевидные решения!

«В программировании есть только две сложные вещи: инвалидация кэша, выбор имени переменной, и ошибки на единицу».
(Джефф Этвуд, создатель StackOverflow).
Apply patch 6_07_update_cache
< 5-е занятие 7-е занятие>
Контакты: Григорий Кислин
E-mail: admin@javaops.ru
ОГРНИП: 317784700063201 | ИНН: 782581076920

Cайт-партнер: topjava.ru
Поделиться:
Москва Санкт-Петербург Киев Минск Харьков Новосибирск Львов Нижний Новгород Алматы Одесса Днепр Краснодар Екатеринбург Самара Ростов-на-Дону Днепропетровск Казань Воронеж Челябинск Пермь Гомель Владивосток Астана Томск Саратов Гродно Уфа Калининград Николаев Запорожье Ярославль Омск Кемерово Белгород Брест Ташкент Херсон Ижевск Чебоксары Караганда Волгоград Балашиха Йошкар-Ола Киров Барнаул Калуга Иркутск Магнитогорск Донецк Монреаль Warszawa Los Angeles Винница Сыктывкар Тюмень Рига Кишинев Бишкек Владимир Красноярск Ульяновск Жуковский Тольятти Тверь Вологда Улан-удэ Сочи Иваново Мариуполь Пенза Краков Сумы Подольск Тула Рязань Хабаровск Helsinki Могилев Haifa Полтава Сургут Новокузнецк Березники San Francisco Иннополис Tel Aviv Ереван Тернополь Ставрополь Кривой рог Северодвинск Витебск Астрахань

AltStyle によって変換されたページ (->オリジナル) /