Интеграционное тестирование в проекте
В данном документе представлено видение интеграционного тестирования проекта.
Мотивация
При работе над задачами важно обеспечивать качество внесенных изменений. Это требует локального регрессионного тестирования системы. В случае сервисов с HTTP-эндпоинтами такое тестирование можно выполнить вручную через Postman или аналогичные инструменты. Однако если код модифицируется повторно, каждый разработчик должен проводить регрессионное тестирование заново.
Один из способов автоматизировать этот процесс — написание тестов, проверяющих поведение приложения через публичный API с использованием моков для внешних зависимостей (внешних сервисов). Эти тесты должны быть интегрированы в цикл разработки.
Ниже представлены ключевые элементы предлагаемого подхода.
Arrange-Act-Assert
При написании тестов предлагается использовать методологию Arrange-Act-Assert.
Изоляция тестов на уровне данных
Хорошо описано в статье Eradicating Non-Determinism in Tests:
There are a couple of ways to get isolation - either always rebuild your starting state from scratch, or ensure that each test cleans up properly after itself. Some people prefer to put less emphasis on isolation and more on defining clear dependencies to force tests to run in a specified order. I prefer isolation because it gives you more flexibility in running subsets of tests and parallelizing tests.
Тесты должны быть изолированными. Это не означает, что они не могут разделять ресурсы, например общую базу данных. Однако работа одного теста не должна влиять на выполнение другого. В большинстве случаев изоляция достигается за счет уникальных идентификаторов сущностей.
Пример генератора:
private val idx = AtomicInteger()
❗️Выбор способа изоляции зависит от практической необходимости. Если невозможно достичь изоляции в рамках одного контекста приложения, тесты стоит вынести в отдельный контекст с использованием
@DirtiesContext
.
Проверка через API
Речь идет об интеграционных тестах, в которых поднимается все приложение. Внешние зависимости, такие как базы данных и Temporal, стартуют через Testcontainers, а внешние сервисы мокируются с помощью WireMock. Взаимодействие происходит через публичный API в рамках black-box тестирования (насколько это возможно).
Предлагается использовать единый уровень представления для API-вызовов, проверки ответов и мокирования внешних сервисов. В части API-вызовов это выражается в использовании embedded строк с json представлениями запросов и ответов.
❗️Не все аспекты системы стоит проверять таким способом. Главное правило: если речь идет о контракте сервиса, то, скорее всего, можно.
WireMock Suite
Набор включает:
- DSL для мокирования
- Механизм захвата исходящих запросов
DSL для мокирования
Базовое описание моков в WireMock многословно, поэтому предлагается DSL, представленный в [wiremock-api-definition.kt].
Пример:
val methodCaptor = serverMock.method(withSuccess("""{}"""))
^ ^ ^ ^
|- объект захвата | | |- описание ответа от мока
| | - микирование
| - объект WireMockServer
Пример с задержкой:
val methodCaptor = serverMock.method(
withDelay(Duration.parse(delay), withSuccess("""{}"""))
)
Пример с цепочкой ответов:
val methodCaptor = serverMock.method(
withConnectionReset(),
withSuccess("""{}""")
)
Механизм захвата исходящих запросов
Для удобства следования паттерну Arrange-Act-Assert предлагается механизм “капторов”, реализованный в [wiremock-suite].
- Описываем объект захвата
- Выполняем шаг “Act”
- Проверяем объекта захвата на этапе “Assert”
Примечания
Объекты захвата в Wiremock
Что значит “удобно” и почему автор считает, что базовая реализация Wiremock
не удобна и часто перегружается, осложняя
поддержку, описано в статье.