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

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