Тестирование контрактов микросервиса

Чтобы убедиться, что все микросервисы хорошо работают вместе, их необходимо протестировать. Работа с разными версиями микросервисов или разрешение конфликтов контрактов может быть проблематичным в больших командах. Поэтому, когда ваши микросервисы действительно содержат формальную спецификацию API, лучше превратить ее в контракт. Контракт гарантирует, что каждая служба работает изолированно, но отправляет правильные запросы своему партнеру.

Давайте рассмотрим взаимодействия сервиса, который мы для простоты назовем Provider (обладающий API).

Когда мы выкатываем новую версию Provider-а мы должны быть уверены, что не поломали совместимость со всеми Consumer-aми:

Требования Consumer-a:

  1. знать что Provider не поломал контракт
  2. знать что он сам (Consumer) не ломает контракт

Требования Provider-а:

  1. не ломать обратную совместимость контракта
  2. знать, какие контракты используются (чтобы понимать, какие эндпоинты уже не нужно поддерживать)

Способы реализации

Реализовать требования можно несколькими способами, давайте коротко рассмотрим каждый.

Способ - интеграционные тесты

Интеграционные UNIT-тесты проверяют, правильно ли код Consumer-a генерирует запрос и обрабатывает ожидаемый ответ, по шагам это выглядит так:

  1. сделать запрос к Provider-у в рамках контракта
  2. получать респонс в рамках контракта
  3. корректно обработать респонс (какая-то бизнес логика обработки полученных данных)

Коротко: надежно, но тесты выполняются долго + но нужна инфраструктура, а это ресурсы, которых всегда не хватает.

Развернуто: все понимают, что когда сервис Consumer используя E2E тестирование мы получаем больше гарантии, но если сервис А зависит от сервиса B, то E2E тестирование потребует наличия уже двух реально работающих зависимостей (A и B), это называется интеграционным тестированием (мы этого не хотим, потому что мы хотим катить микросервисы обособленно). Когда в организации тысячи микросервисов и сотни команд, работающих с ними, при попытке использовать подход возникают проблемы:

  • когда Customer хотел добавить новый тест, требующий нового состояния Provider-a, он блокировался, ожидая, пока команда поставщика добавит требуемое состояние (иногда это может занять несколько недель, особенно если две команды находятся в разных часовых поясах и не могут созвониться и договориться)
  • не все команды имеют одинаковый уровень надежности сборки, поэтому, когда Customer, который предъявляет очень строгие требования к надежности сборки, интегрируется с Provider-ом, который этого не делает, это создает проблемы (Вы можете не найти способ стандартизировать сборки в достаточной степени, чтобы сделать эту оркестровку простой и недорогой при первоначальной настройке)
  • проблема несогласованного ожидания скорости сборки между командами - небольшие быстрые микросервисы не хотят ждать 45 минут, пока медленный устаревший провайдер будет собираться и подниматься

Способ - тестирование схем контрактов

Сверяются yaml/json схемы, это не требует инфраструктуры и тесты получаются очень быстрыми.

Вывод: способ обязателен к реализации, т.к. почти не потребляет ресурсов.

Способ - замещение партнера

Чтобы реализовать данный способ нам нужен сервис Mock (при тестировании контрактов использующих message queue называемый Mock-брокером).

Mock - сервис является источником знаний о всех контрактах (знает кто и кому обязуется соблюдать контракт) и на основании этих знаний тестирует Consumer-а и Provider-а. В рамках тестирования всех сервисов является глобальным (чтобы к контрактам можно было легко получить доступ для тестирования любого набора микросервисов). 

Коротко: потребляет мало ресурсов, правда менее надежен, интеграционные тесты

Развернуто: Mock - сервис:

  1. знает контракты Provider-а (продакшен и в каждой фича-ветке)
  2. знать контракты Consumer-а (продакшен и в каждой фича-ветке)
  3. знает о маппинге реквестов и респонсов для прохождения тестов (ветки используемой в продакшен и в каждой фича-ветке), т.е. может возвращать заранее определенный фиктивный ответ на основании реквеста (если реквест А, то респонс А)
  4. перед деплоем сопоставляет знания Provider-а и Consumer-а (ведь когда фича-ветка замержена в основную ветку, то тесты уже не прогоняются, хотя может быть стоило)

заметка: я бы назвал Mock сервис Spoofer (подделка), ниже на изображениях он называется просто Mock (e.g. Pact)

Дополнительный функционал Mock-сервиса:

  1. тестируя Consumer-а, Mock-сервис может генерировать респонсы (валидные и невалидные), чтобы помогать в разработке функционала
  2. тестируя Provider-а, Mock-сервис может генерировать реквесты (валидные и невалидные) и проверять ожидания (например верный HTTP-статус)

Чтобы попробовать понять ситуацию целиком, рассмотрим пример когда Consumer является законодателем контракта:  

Provider Testing Tool (BYO - bring your own) - инструмент, который Provider использует для тестирования себя.

Важно: последовательность указанная на схеме выше не является обязательной, это лишь пример того, как:

  1. Consumer создает в Mock-сервисе респонс для своего запроса + проверяет работу Mock-сервиса используя свой Unit-тест
  2. Consumer публикует контракт в Mock-сервисе
  3. Provider читает контракт и настраивает BYO + проверяет работу Mock-сервиса используя свой Unit-тест
  4. Provider публикует контракт в Mock-сервисе
  5. Mock-сервис проверяет совместимость контрактов (о том, как он это делает, будет написано ниже)

На практике процесс тестирования разделяется на 2 части (тестирование Consumer-a и тестирование Provider-а), коротко:

Тестирование Consumer-а

Рассмотрим процесс прохождения тестов, ориентированных на Consumer-a:

Тестирование Provider-а

Рассмотрим процесс прохождения тестов, ориентированных на Provider-a:

Данное тестирование реализовывается на основе запросов, которые делает Customer (но без Customer-a) с помощью Mock-сервера.

Чтобы Provider мог обработать тестовые запросы, он должен быть поднят, сконфигурирован и иметь состояние (данные в результате которых, он сможет вернуть правильный response), данное состояние он может получать до начала тестирования производимое Mock-сервером, например:

  1. если Provider пользуется бд, то база данных должна быть поднята и наполнена данными, либо класс работы с бд должен быть подменен сконфигурированным Mock-классом
  2. если Provider пользуется сторонним сервисом, то класс работы с сторонним сервисом должен быть подменен сконфигурированным Mock-сервером (WireMock или свой собственный MockProvider) или Mock-классом

Желательно: каждое взаимодействие должно проверяться изолированно, без сохранения контекста предыдущих взаимодействий.

Доменные события

Сервисы часто публикуют доменные события, потребляемые другими сервисами (одним или несколькими). При тестировании нужно убедиться в том, что издатель и его подписчики согласовали канал сообщений и структуру доменных событий.

Тестирование доменного сообщения

Участник - Consumer или Provider, чье доменное сообщение тестируется одним из двух способов:

  • Участник публикует доменное событие в брокере, а Mock-сервис потребляет событие и проверяет его согласно контракту
  • Mock-сервис публикует доменное событие в брокере и Участник потребляет доменное событие

Важно: вместо брокера обычно используется Mock-сервис.

Асинхронные запросы/ответы

Тестирование сервисов, которые взаимодействуют с использованием асинхронных запросов/ответов похоже на синхронное тестирование, но обычно асинхронные сервисы (работая например по принципу CQRS) возвращают response говорящий о том, что сообщение принято, но не говорит о том что:

  1. сообщение валидно
  2. сообщение успешно обработано (сохранено)

В этом случае Mock-сервис должен проверить пункт 2 (тем самым пункт 1 будет проверен тоже).

Тестирование запроса и результата

Участник - Consumer или Provider, чей результат запроса тестируется так:

  • Участник отправляет запрос в Mock-сервис
  • Mock-сервис проверяет запрос согласно контракту, и сохраняет результат в своем хранилище из которого Участник может получить данный результат
  • Участник получает результат обработки своего запроса  (например дополнительным http-запросом или потреблением из брокера)

Важно: вместо брокера обычно используется Mock-сервис.

Хорошие идеи

1. Тестировать неожиданное поведение, например невалидные данные реквеста, респонса (статусы и другие заголовки, нарушение структур и типизацию данных, выходы за диапазоны возможных значений и т.п.): 

  • тестировать Consumer-a на невалидные респонсы
  • тестировать Provider-a на невалидные реквесты

2. Маппинг реквестов и респонсов лучше делать вручную (не стоит делать его с помощью автоматической записи, основываясь на реквестах и респонсах, ведь они могут быть быть неверными)

Полезные ссылки:

  1. OPENApi стандарт спецификации REST API
  2. ReDoc - красивая документация из yaml-файла
  3. https://github.com/pact-foundation/pact-php
  4. https://github.com/thephpleague/openapi-psr7-validator - выполнение тестирования контрактов на основе спецификаций
  5. WireMock - только имитирует реквест / респонс
  6. https://github.com/pactflow/example-consumer
  7. https://github.com/pact-foundation/pact-php
  8. https://bitbucket.org/atlassian/openapi-diff/src/master/ реализация дифов свагера на тайпскрипт
  9. prism
  10. wiremock
  11. phiremock-server

Источник: 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12


10.09.2021 21:22