Сервис заказов вызывает сервис доставки, но общий интеграционный стенд доступен только перед релизом. Как контрактное тестирование позволит раньше обнаружить несовместимость интерфейсов?
consumer: OrderService
provider: DeliveryService
request:
method: GET
path: /delivery/42
response:
status: 200
body:
eta: "2025-03-10"
price: 499
Контрактное тестирование проверяет, что поставщик сервиса сохраняет интерфейс, от которого зависит потребитель. Потребитель фиксирует ожидаемые запросы и ответы в контракте, а тесты поставщика автоматически проверяют соответствие этому контракту ещё до общего интеграционного запуска.
Если сервис доставки вместо поля eta начнёт возвращать deliveryDate, проверка контракта обнаружит несовместимость независимо от того, доступен ли интеграционный стенд. Это сокращает время обратной связи, но не заменяет полноценные интеграционные и end-to-end-проверки.
Подход получил распространение в системах с независимо разворачиваемыми сервисами, где команды меняют компоненты независимо друг от друга. Полная интеграционная проверка таких систем часто требует сложной инфраструктуры, согласованного расписания и стабильных зависимостей.
Контрактное тестирование решает исходную проблему позднего обнаружения несовместимых изменений: проверка интерфейса выполняется рядом с разработкой и сборкой конкретного сервиса, а не только в общем окружении перед релизом.
Потребитель может ожидать HTTP-ответ со статусом 200 и полями eta и price. Если поставщик переименует поле, изменит его тип или начнёт возвращать другой статус, компонент поставщика может проходить собственные тесты, а потребитель — перестать работать.
Без отдельной проверки контракта ошибка обнаружится только при интеграционном запуске или в продакшене. Неверный контракт также опасен: если он описывает не реальные ожидания потребителя, тесты могут быть зелёными, но не защищать рабочий сценарий.
Обычно потребитель формирует контракт потребителя на основе реально используемого сценария. В приведённом примере он фиксирует метод, путь, статус и структуру значимых полей ответа. Поставщик запускает проверку, которая подготавливает ответ на такой запрос и сравнивает его с контрактом.
Проверка должна учитывать не только наличие полей, но и их типы, обязательность, допустимые форматы, HTTP-статусы и важные заголовки. Например, изменение price с числа на строку может нарушить десериализацию, даже если само поле осталось на месте.
Преимущество подхода — быстрый и локальный сигнал о нарушении совместимости. Поставщик может проверять несколько контрактов разных потребителей, а потребитель — публиковать контракт как артефакт сборки.
Ограничение состоит в том, что контракт покрывает только зафиксированные взаимодействия. Он не доказывает корректность бизнес-логики, производительность, отказоустойчивость, безопасность или совместимость всех возможных запросов. Поэтому нужны дополнительные компонентные, интеграционные и системные проверки.
Контракт следует изменять согласованно с версионированием интерфейса. При обратно совместимом расширении, например добавлении необязательного поля, проверка обычно не должна ломаться; удаление обязательного поля или изменение его типа требует отдельного решения о совместимости.
Минимальная идея проверки может быть представлена так:
Здесь поставщик обязан вернуть ответ, совместимый с описанной структурой. Если он возвращает price: "499" или удаляет eta, проверка поставщика должна завершиться ошибкой.
Команда сервиса заказов зависела от сервиса доставки. Вариант с единственным общим стендом давал реалистичную проверку, но обратная связь приходила поздно: стенд часто был занят, а ошибка могла быть связана с окружением. Вариант с ручной сверкой документации был дешевле, но зависел от внимательности и не гарантировал исполнения требований.
Команда выбрала контрактные проверки для критичных запросов, а общий стенд оставила для сквозных сценариев. После изменения поставщиком формата даты проверка контракта упала на этапе сборки сервиса доставки; команда либо сохранила обратную совместимость, либо выпустила согласованную версию интерфейса. В результате конкретная несовместимость была обнаружена до релизной интеграции, но сквозные проверки продолжили выявлять ошибки, которых контракт не описывал.
Вопрос: Кто должен определять контракт — поставщик или потребитель?
Ответ: Для сценариев, ориентированных на потребителя, контракт обычно формируется из требований потребителя: именно он определяет, какие поля и статусы реально использует. Поставщик затем подтверждает, что способен это предоставить. Это предотвращает ситуацию, когда поставщик описывает богатый интерфейс, но не замечает нарушение минимального ожидания клиента.
Вопрос: Заменяет ли контрактное тестирование интеграционный тест?
Ответ: Нет. Контракт проверяет совместимость границы взаимодействия, но не реальное взаимодействие всех компонентов в общем окружении. Он может не обнаружить ошибки маршрутизации, авторизации, сетевых таймаутов, конфигурации, последовательности вызовов или согласованности данных между несколькими сервисами.
Вопрос: Что произойдёт, если в контракт включить весь ответ API?
Ответ: Проверка станет хрупкой: любое безопасное добавление поля или изменение неиспользуемой детали может создавать ложные сбои. Контракт должен описывать значимые для потребителя обязательства, а не случайную полную копию текущего ответа. При этом слишком узкий контракт пропустит важное изменение, поэтому его состав нужно связывать с реальными сценариями использования и регулярно пересматривать.