Контрактное тестирование: как поймать нарушенные API-обещания до попадания в продакшен
Вы деплоите изменение в сервисе пользователей. Все тесты проходят. Пайплайн зелёный. Через пять минут сервис уведомлений начинает сыпать ошибками в продакшене. Пользователи видят пустые экраны. Корневая причина? Вы удалили поле из ответа API, которое ваш собственный сервис никогда не использовал, но от него зависел сервис уведомлений.
Интеграционные тесты этого не поймали. Обе команды запустили свои тесты, всё прошло. Проблема проявилась только когда сервисы реально заговорили друг с другом в продакшене. Это пробел, который закрывает контрактное тестирование.
Настоящая проблема, которую пропускают интеграционные тесты
Интеграционные тесты проверяют, что сервисы могут соединиться и обменяться данными. Но они не гарантируют, что соглашение между сервисами остаётся в силе, когда одна сторона меняется. Две команды могут иметь идеально зелёные интеграционные тесты независимо, но сломать системы друг друга при следующем деплое.
Взгляните на это так: Команда A запускает сервис пользователей. Команда B запускает сервис уведомлений, который потребляет данные пользователей. Команда A решает почистить ответ API, удалив поле, которое выглядит неиспользуемым внутри их собственного кода. Их интеграционные тесты проходят, потому что они тестируют свой собственный сервис. Интеграционные тесты команды B тоже проходят, потому что они тестируют против мока или снимка старого API. Нарушенный контракт проявляется только когда оба сервиса запускаются вместе в стейджинге или продакшене.
Что на самом деле делает контрактное тестирование
Контрактное тестирование делает неявное соглашение между сервисами явным и проверяемым. Каждый раз, когда два сервиса общаются через API, существует договорённость о том, что отправляется и что принимается. Контрактное тестирование превращает эту договорённость в автоматизированные проверки.
Концепция работает с двумя ролями:
Следующая диаграмма последовательности показывает, как провайдер публикует контракт, а потребитель проверяет его перед деплоем:
- Провайдер: Сервис, который предоставляет API
- Потребитель: Сервис, который вызывает API
Провайдер объявляет, что он гарантирует доставить. Потребитель объявляет, что ему на самом деле нужно. Пока обе стороны согласны, контрактный тест проходит. Когда что-то меняется, тест ломается до того, как изменение попадёт в продакшен.
Большинство команд используют подход, управляемый потребителем. Потребитель определяет свои ожидания в файле контракта. Затем провайдер проверяет, удовлетворяет ли его API все контракты от всех потребителей. Если изменение провайдера нарушает какой-либо контракт, команда провайдера узнаёт немедленно. Они могут поговорить с командой потребителя, скорректировать изменение или версионировать API, не ломая существующих потребителей.
Почему контрактные тесты быстрее интеграционных
Самое большое практическое преимущество — скорость и независимость. Контрактные тесты не требуют запуска других сервисов. Им не нужна реальная база данных или полное стейджинговое окружение. Вы просто запускаете провайдера с предопределёнными тестовыми данными и проверяете, соответствует ли ответ контракту.
Контрактный тест выполняется за секунды. Интеграционный тест, который поднимает несколько сервисов и баз данных, занимает минуты. В CI-пайплайне эта разница имеет значение. Вы можете запускать контрактные тесты рано и быстро падать, не дожидаясь дорогих интеграционных наборов.
Контрактные тесты также помогают с внешними зависимостями, которые вы не контролируете. Если ваше приложение вызывает сторонний API, вы можете написать контрактный тест, который проверяет, возвращает ли внешний API ожидаемый формат. Когда третья сторона меняет свой API, ваш контрактный тест падает, и вы узнаёте об этом до того, как пострадают пользователи.
Что контрактные тесты не покрывают
Контрактные тесты проверяют только формат и структуру. Они проверяют, что нужные поля существуют с правильными типами. Они не проверяют, корректны ли данные с бизнес-точки зрения. Они не тестируют стабильность сети, аутентификацию в продакшене или время ответа под нагрузкой.
Для этих аспектов вам всё ещё нужны интеграционные тесты. Контрактные тесты и интеграционные тесты дополняют друг друга, а не заменяют. Контрактные тесты выявляют структурные несоответствия на раннем этапе. Интеграционные тесты выявляют проблемы времени выполнения и данных позже.
Где контрактные тесты разместить в пайплайне
Размещайте контрактные тесты после модульных тестов и перед интеграционными. Этот порядок имеет смысл, потому что контрактные тесты быстрее интеграционных, но дают дополнительную уверенность, что сервисы, которые вы собираетесь тестировать вместе, всё ещё совместимы. Если контрактный тест падает, нет смысла запускать дорогие интеграционные тесты, которые, скорее всего, тоже упадут.
Вот типичный порядок пайплайна:
- Модульные тесты
- Контрактные тесты
- Интеграционные тесты
- Сквозные тесты (если необходимо)
С чего начать
Не пытайтесь добавить контрактные тесты сразу для всех сервисов. Начните с сервисов, которые меняются чаще всего и вызывают больше всего проблем при общении с другими сервисами. Это точки трения, где команды часто ломают друг друга.
Ищите эти сигналы:
- Сервисы, где изменение одной команды часто ломает функциональность другой команды
- API, от которых зависят несколько потребителей
- Сервисы, которые регулярно меняют формат ответа
- Внешние API, которые неожиданно менялись в прошлом
Сосредоточьтесь на них в первую очередь. Как только контрактные тесты заработают и начнут ловить реальные проблемы, постепенно расширяйтесь на другие сервисы.
Практический чек-лист перед добавлением контрактных тестов
- Определите три пары сервисов, которые вызывают наибольшее количество кросс-командных поломок
- Решите, использовать ли контракты, управляемые потребителем или провайдером
- Выберите инструмент контрактного тестирования, подходящий под ваш стек (Pact, Spring Cloud Contract или аналогичный)
- Начните с одного провайдера и одного потребителя
- Запускайте контрактные тесты в CI перед интеграционными тестами
- Настройте процесс уведомления команд при нарушении контракта
Вывод
Контрактное тестирование выявляет несовместимость API в момент внесения изменения, а не после деплоя. Оно работает быстро, не требует полного окружения и даёт командам раннее предупреждение до того, как нарушенные обещания попадут в продакшен. Начните с самых болезненных границ сервисов, автоматизируйте контракты и позвольте пайплайну сообщать вам, когда соглашение нарушено. Ваши пользователи никогда не узнают разницы, и в этом вся суть.