Документирование. Тестирование. Кэширование
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 аннтоции)
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).
Проверить работу кэша можно по логам обращения к базе:
- GET http://localhost:8080/api/account (authorize with: user@gmail.com/password)
- GET http://localhost:8080/api/account // cached
-
PUT http://localhost:8080/api/account Content-Type: application/json Authorization: Basic user@gmail.com password { "id": 1, "email": "user@gmail.com", "firstName": "User_First_Update", "lastName": "User_Last_Update", "roles": [ "USER" ] } ####
- GET http://localhost:8080/api/account // cached
- GET http://localhost:8080/api/account // cached
Apply patch 6_06_add_cache
При изменения пользователей админом кэш также следует инвалидировать: добавил аннотации кэширования в UserRepository
. Здесь следует быть осторожным, потому что мы
не
учли все возможные случаи в репозитории
(saveAll, saveAndFlush, deleteInBatch, ...
), а также при удалении по id
инвалидируется весь кэш.
Наконец последнее - кэш часто мешает тестам и его надо чистить перед каждым тестом. Или, как мы сделали на TopJava18, вообще отключать кэш в тестах:
запускаем тесты с профилем @ActiveProfiles("test")
и отключаем его в application-test.yaml
, см. Profile Specific
Files.
Вообще кэширование рекомендуется делать в сервисах, что исключает автогенерацию Spring Data REST.
И обычно от него много проблем,
поэтому в тестовом приложении не делайте кэши "на всякий случай", только очевидные решения!
«В программировании есть только две сложные вещи: инвалидация кэша, выбор имени переменной, и ошибки на единицу».
(Джефф Этвуд, создатель StackOverflow).