awesome-everything EN
↑ Обратно к восхождению

Инженерная практика

Верификация провайдера и брокер: переигрывание ожиданий консьюмера против реального сервиса

Суть Консьюмер опубликовал pact; теперь провайдер переигрывает каждое записанное взаимодействие и доказывает каждое. Provider states настраивают данные так, чтобы взаимодействие было выполнимым. Брокер хранит pact''''ы и результаты и через webhooks развязывает два CI-пайплайна.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 17 min

Команда Прии checkout-web опубликовала pact: «когда я делаю GET /prices/42, я читаю amount_cents (число) и currency (строку)». Контракт теперь существует. Но контракт, который никто не проверяет, — лишь пожелание. Сэм владеет сервисом pricing, и в прошлом спринте он переименовал amount_cents в unit_amount — чистый рефакторинг, все собственные тесты pricing зелёные, выкатил в пятницу. В понедельник checkout-web рисовал NaN на каждой странице товара, потому что поле, которое он парсит, тихо исчезло. Pact описывал ровно ту поломку, что выкатилась, и ничто не переиграло его против реального сервиса Сэма до деплоя. Запись так и не переиграли. Это переигрывание — отправка каждого записанного запроса на реально работающий провайдер и проверка того, что ответ всё ещё соблюдает то, что читает консьюмер, — и есть верификация провайдера (provider verification), а место, которое хранит pact’ы и результаты верификации, чтобы двум командам не пришлось координироваться вручную, — это брокер (broker).

Верификация — это переигрывание, а не перечитывание

Pact-файл — это список конкретных взаимодействий: запрос, ожидаемый ответ и provider state, который каждое из них предполагает. Верификация провайдера механична: верификатор Pact берёт этот список и для каждого взаимодействия отправляет записанный запрос на реальный, работающий провайдер и сравнивает фактический ответ с ожидаемым. Со стороны провайдера никакого мока нет — запрос попадает в подлинную маршрутизацию, контроллеры, сериализаторы и (при настроенном состоянии) подлинный слой данных. Если pact checkout-web говорит, что GET /prices/42 должен вернуть тело, содержащее amount_cents, верификатор шлёт ровно этот запрос на поднятый сервис pricing и осматривает, что вернётся.

Сравнение намеренно асимметрично, и именно это свойство делает гейт стабильным. Верификатор проверяет, что фактический ответ содержит как минимум те данные, что описал консьюмер, — минимальный ожидаемый ответ. Сэм может добавить десять новых полей в /prices/42, и верификация останется зелёной, потому что консьюмер никогда не утверждал их отсутствие. Но если он переименует amount_cents, поле, которое читает консьюмер, исчезнет, фактический ответ перестанет содержать минимальную ожидаемую форму, и верификация упадёт до деплоя. Контракт срабатывает ровно на том изменении, что сломало бы реального консьюмера, и молчит обо всём остальном — та же точность, что давала consumer-driven запись, теперь обеспеченная со стороны провайдера.

Provider states — тонкая трудная часть

Взаимодействие вроде «GET /prices/42 возвращает 200 с телом цены» выполнимо, только если цена 42 действительно существует в провайдере в момент переигрывания запроса. Консьюмер записал это предусловие как provider state (состояние провайдера) — строку в клаузе given, например "a price with id 42 exists", Pact-эквивалент шага Given из Cucumber: привести систему в известное состояние перед запуском взаимодействия. Верификация — момент, когда эту строку приходится превратить в реальные данные.

Поэтому провайдер должен зарегистрировать обработчики состояний (state handlers): код, который для каждого именованного состояния настраивает данные, нужные взаимодействию. Перед переигрыванием каждого взаимодействия верификатор вызывает соответствующий обработчик (часто через POST { "consumer": "...", "state": "..." } на тестовый эндпойнт смены состояния), обработчик вставляет цену 42 в хранилище данных провайдера, затем отправляется запрос. Два правила делают это рабочим и легко нарушаются. Во-первых, каждое взаимодействие верифицируется в изоляции — никакое состояние не переносится из предыдущего, так что каждый обработчик должен устанавливать своё полное предусловие с нуля и в идеале убирать за собой после. Во-вторых, обработчик настраивает данные, а не ответ: он делает взаимодействие выполнимым, но ответ по-прежнему порождает реальный код провайдера, в чём весь смысл. Здесь верификация тихо дорожает — каждая отдельная строка given, что написали консьюмеры, становится обработчиком состояния, который команда провайдера обязана реализовать и поддерживать, а отсутствующий или неверный обработчик валит верификацию не потому, что контракт сломан, а потому что предусловие так и не было выполнено.

Почему это работает

Почему бы просто не направить верификацию на общую staging-базу, где цена 42 уже есть? Потому что тогда тест перестаёт быть детерминированным и изолированным: вернёт ли GET /prices/42 200, зависит от того, какие строки случайно существуют в этот день, чужой джоб очистки может удалить цену 42 между прогонами, а взаимодействие, ожидающее 404, не может сосуществовать с ожидающим 200 против одних и тех же фиксированных данных. Provider states переносят предусловие в контракт и в принадлежащий провайдеру код настройки, так что каждое взаимодействие несёт собственный мир и переигрываемо в изоляции на свежезасеянном провайдере. Именно это позволяет верификации прогоняться в собственном CI провайдера, против его собственных эфемерных данных, без координации с окружением консьюмера.

Брокер — система записи между двумя пайплайнами

Верификации нужны два артефакта, чтобы встретиться: pact консьюмера и результат «прошло/упало» провайдера. Наивный способ их свести — подключить CI консьюмера так, чтобы он напрямую запускал сборку провайдера, — но это снова связывает те два пайплайна, ради развязки которых ты и пришёл к контрактному тестированию. Pact Broker (open-source) / PactFlow (хостинг) — обменник, разрывающий эту связь. Консьюмер публикует свой pact в брокер, помеченный версией и веткой, и уходит. Независимо CI провайдера тянет нужные pact’ы из брокера, прогоняет верификацию и публикует результаты обратно. Ни один пайплайн не вызывает другой; брокер — общая, асинхронная система записи, держащая каждый pact и каждый результат верификации.

Брокер также может толкать (push). Webhook срабатывает на событиях брокера — важнее всего contract_requiring_verification_published (который пришёл на смену contract_content_changed в брокере 2.82.0) — чтобы запустить сборку верификации провайдера в момент, когда приходит новый или изменённый pact, передавая шаблонные параметры вроде ${pactbroker.pactUrl} и ${pactbroker.providerVersionNumber}, так что сборка провайдера точно знает, что верифицировать. Итог всего этого — матрица результатов верификации (verification results matrix): сетка того, какие версии консьюмеров были верифицированы против каких версий провайдеров. Эту матрицу запрашивает инструмент can-i-deploy перед релизом — «верифицирована ли версия pricing, которую я собираюсь выкатить, против версии checkout-web, уже работающей в проде?» — так что каждый pacticipant (слово Pact для приложения в контракте) может деплоиться по собственному расписанию, под гейтом из записанных результатов, а не из человека, сверяющегося с другой командой.

АспектБез брокера: пайплайны связаны напрямуюБрокер как система записи
Передача pactЗакоммичен в репо провайдера / передан вручнуюОпубликован в брокер, помечен версией + веткой
Кто запускает верификациюCI консьюмера вызывает сборку провайдера напрямуюWebhook брокера на contract-requiring-verification
Связанность пайплайновТесная — сборка консьюмера блокируется на провайдереАсинхронная — ни один пайплайн не вызывает другой
Решение о деплоеВручную «зелёная ли сборка другой команды?»can-i-deploy запрашивает матрицу результатов
Независимый деплойНет — оба должны релизиться вместеДа — каждый pacticipant по своему расписанию
Расставь шаги по порядку

Упорядочьте сквозной поток от нового pact консьюмера до гейтированного деплоя провайдера:

  1. 1 CI консьюмера публикует pact в брокер, помеченный его версией и веткой
  2. 2 Брокер шлёт webhook contract_requiring_verification_published в CI провайдера
  3. 3 Провайдер поднимается, и для каждого взаимодействия запускает обработчик состояния, чтобы настроить данные, предполагаемые его `given`
  4. 4 Верификатор переигрывает каждый записанный запрос и проверяет, что ответ содержит минимальную ожидаемую форму
  5. 5 Результаты верификации публикуются обратно в брокер, заполняя матрицу
  6. 6 Перед релизом can-i-deploy запрашивает матрицу, чтобы подтвердить: эта версия провайдера верифицирована против консьюмера из прода
Викторина

Во время верификации провайдер добавляет в GET /prices/42 три новых поля, которые pact консьюмера никогда не упоминает. Что произойдёт и почему?

Викторина

У взаимодействия `given` — 'a price with id 42 exists', но команда провайдера так и не написала обработчик для этого состояния. Каков результат и его правильное прочтение?

Выбери лучший вариант

Как провайдер должен подготовить данные, чтобы каждое записанное взаимодействие было выполнимо во время верификации?

Вспомните перед уходом
  1. 01
    Объясните полный механизм верификации провайдера, включая сравнение по минимальному ответу и роль provider states.
  2. 02
    Для чего нужен брокер и как webhooks плюс can-i-deploy позволяют двум командам деплоиться независимо?
Итог

Консьюмер опубликовал pact, но контракт, который никто не переигрывает, — лишь пожелание: переименование, вернувшее NaN, выкатилось, потому что ничто не переиграло запись против реального провайдера. Верификация провайдера — это и есть переигрывание: верификатор шлёт каждый записанный запрос на подлинный работающий провайдер и проверяет, что фактический ответ содержит как минимум минимальную ожидаемую форму, так что гейт остаётся зелёным, когда провайдер добавляет поля, которые никто не читает, и срабатывает лишь когда поле, которое консьюмер действительно парсит, переименовано, удалено или сменило тип. Тонкая трудная часть — provider states: каждое взаимодействие объявляет своё предусловие как строку given, и провайдер должен зарегистрировать обработчики на каждое состояние, засевающие ровно те данные — запускаемые перед каждым взаимодействием, в изоляции, настраивающие данные, а не ответ, — потому что направление верификации на общие staging-данные разрушает детерминизм, который контракт призван гарантировать. Связывает всё брокер (Pact Broker или PactFlow), асинхронная система записи, держащая каждый pact и каждый результат верификации, чтобы ни одному CI-пайплайну не пришлось вызывать другой; webhooks вроде contract_requiring_verification_published запускают сборку верификации провайдера, когда pact меняется, результаты накапливаются в матрицу верификации, а can-i-deploy запрашивает эту матрицу, чтобы каждый pacticipant мог деплоиться независимо — под гейтом из записанного доказательства, а не из двух команд, координирующихся вручную. Дальше: как контракты безопасно эволюционируют, пока обе стороны меняются со временем.

Связанные уроки
Продолжить восхождение ↑can-i-deploy и версионирование контрактов: гейт безопасности деплоя
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources5
expand
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05

Trademarks belong to their respective owners. Editorial reference only.