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

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

Consumer-driven контракты: истину объявляет консьюмер

Суть Consumer-driven контракт — исполняемая спецификация из проходящего теста консьюмера, а не ручной документ. Консьюмер объявляет, что ему нужно, против мока; это даёт pact-файл реальных взаимодействий, публикуемый в брокер. Провайдеру говорят, что соблюдать, а не дают гадать.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 16 min

Прия владеет фронтендом checkout-web; Сэм владеет API pricing. Сэм держит красивый OpenAPI-документ, но в этом квартале он дважды оказывался неверным — однажды описал поле, которое так и не выкатили, однажды упустил поле tax_cents, от которого уже зависели три команды. Документ ничем не исполняется, поэтому ничто не заставляет его совпадать с кодом. Команда Прии наконец перестаёт читать документ Сэма и вместо этого пишет крошечный тест: «когда я делаю GET /prices/42, я читаю amount_cents (число) и currency (строку)». Этот тест, запущенный однажды, выдаёт JSON-файл, описывающий ровно это взаимодействие. Теперь есть артефакт, говорящий, что на самом деле нужно checkout-web, — и его можно переиграть против реального сервиса Сэма, чтобы доказать, что это всё ещё держится.

Кто пишет истину и почему это консьюмер

В прошлом уроке недостающий слой должен был проверять границу без подъёма обоих сервисов. Первое проектное решение: кто объявляет, какой должна быть граница? Есть два ответа. Provider-driven (spec-first): провайдер публикует OpenAPI-документ, и от консьюмеров ждут соответствия. Consumer-driven: каждый консьюмер объявляет, в исполняемой форме, ровно те запросы, что он делает, и те части ответа, что он читает, а провайдер обязан соблюдать объединение всего этого.

Consumer-driven выигрывает по конкретной причине: он фиксирует фактическое использование, а не объявленную поверхность. API pricing Сэма может возвращать сорок полей; checkout-web читает четыре. Consumer-driven контракт записывает эти четыре. Это делает контракт точным утверждением «что на самом деле кого-то сломает» — Сэм волен свободно менять остальные тридцать шесть полей, и только изменение одного из четырёх трогает гейт. Provider-driven спецификация, напротив, помечает каждое изменение одинаково, включая те, до которых ни одному консьюмеру нет дела, что порождает шум и подрывает доверие. Консьюмер — единственная сторона, знающая, от чего он действительно зависит; consumer-driven контракты делают это знание исполняемым.

Pact-файл — это запись, а не список желаний

Артефакт в центре всего этого — pact-файл, в Pact (де-факто открытом стандарте) это JSON-документ. Ключевое свойство — как он производится: он не пишется вручную, он порождается как побочный эффект проходящего теста консьюмера. Механизм:

  1. Тест консьюмера прогоняется против Pact mock server — локального HTTP-сервера, который поднимает Pact. Тест его конфигурирует: «ожидай GET /prices/42; ответь 200 с этим телом». Затем реальный клиентский код консьюмера вызывает этот мок.
  2. Если код консьюмера действительно делает заявленный запрос и парсит заявленный ответ, тест проходит — и Pact записывает прогнанное взаимодействие в pact-файл. Если код консьюмера разошёлся (перестал слать Accept: application/json или вообще не читает поле), ожидание мока падает и pact не порождается, так что нельзя опубликовать контракт, который ты не соблюдаешь.

Это и есть инверсия, делающая контракты надёжными там, где документы нет. Документ — это обещание, написанное отдельно от кода; ничто его не исполняет, поэтому он тихо гниёт. Pact-файл — это запись кода, который выполнился и прошёл. Он не может описать взаимодействие, которого консьюмер не совершает, потому что выдаётся лишь когда собственный тест консьюмера его прогоняет.

АспектOpenAPI / wiki-документ вручнуюConsumer-driven pact-файл
Кем написанПровайдером, заявляющим всю поверхностьКонсьюмером, заявляющим фактическое использование
Как производитсяПишется отдельно от кодаВыдаётся проходящим тестом консьюмера
Риск дрейфаТихо гниёт; ничто его не исполняетНе может описать то, чего код не делает
Сигнал об измененииПомечает каждое поле одинаково (шумно)Срабатывает лишь на потребляемых полях

Сопоставляй форму, а не точные значения

Наивная запись была бы переуточнённой: если pact фиксирует amount_cents ровно на 1299, то любой ответ с другой ценой провалит верификацию — хотя консьюмеру нет дела до значения, лишь до того, что это число. Pact решает это матчерами. Вместо «тело равно этому точному JSON» консьюмер объявляет «в теле есть поле amount_cents, которое целое, поле currency, которое строка по шаблону [A-Z]{3}, и массив items с хотя бы одним элементом такой формы». Верификация тогда проверяет тип и структуру, а не буквальные примеры значений.

Это важно, потому что проводит границу контракта в правильном месте: он утверждает то, на что консьюмер действительно опирается (число присутствует и парсится), и молчит о том, на что нет (какое именно число). Переуточнение точными значениями возвращает хрупкость — контракт падает на безобидных изменениях данных. Недоуточнение (например, проверка лишь наличия поля, а не типа) пропускает реальную поломку. Хорошие контракты используют матчеры типов для частей, которые консьюмер парсит, и оставляют всё остальное неограниченным.

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

Provider states (состояния провайдера) — вторая половина того, почему pact переигрываем. Взаимодействие консьюмера часто предполагает существование данных: «GET /prices/42 возвращает 200» держится, только если цена 42 существует. Pact записывает строку providerState вроде "a price with id 42 exists". Во время верификации (следующий урок) провайдер настраивает это предусловие перед переигрыванием запроса, так что тест детерминирован и не зависит от того, что случайно лежит в общей базе. Контракт несёт собственный контракт настройки; именно это позволяет ему прогоняться против реального провайдера в изоляции.

Расставь шаги по порядку

Упорядочьте, как возникает consumer-driven pact-файл:

  1. 1 Тест консьюмера конфигурирует Pact mock server: «ожидай GET /prices/42, ответь 200 с этой формой»
  2. 2 Реальный клиентский код консьюмера вызывает мок и парсит ответ
  3. 3 Тест ассертит нужные ему поля, используя матчеры типов, а не точные значения
  4. 4 Тест проходит, поэтому Pact записывает прогнанное взаимодействие (с provider states) в pact-файл
  5. 5 Pact-файл публикуется в брокер, помеченный версией и веткой консьюмера
Викторина

Почему consumer-driven pact-файл надёжнее, чем OpenAPI-документ, поддерживаемый вручную?

Викторина

Консьюмер читает лишь то, что amount_cents — число. Что должен утверждать о поле его pact?

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

Ваш провайдер возвращает 40 полей; ваш консьюмер читает 4. Как ограничить контракт?

Вспомните перед уходом
  1. 01
    Объясните полный механизм, которым производится pact-файл, и почему это делает его надёжнее OpenAPI-документа.
  2. 02
    Почему матчить по типу, а не по точным значениям, и какую роль играют provider states?
Итог

Недостающему слою границы нужен кто-то, кто объявит, какова эта граница, и консьюмер — правильный автор, потому что лишь он знает, от чего действительно зависит. Consumer-driven контракт записывает фактическое использование — те несколько полей, что консьюмер читает из скольких бы то ни было, что возвращает провайдер, — так что провайдер волен свободно эволюционировать всё остальное, и гейт срабатывает лишь на изменениях, которые реально сломали бы настоящего консьюмера; provider-driven спецификация, напротив, помечает каждое изменение одинаково и может разойтись с кодом. Артефакт — pact-файл, и его определяющее свойство в том, что он порождается как побочный эффект проходящего теста консьюмера против Pact mock server, а не пишется вручную: он может описывать лишь взаимодействия, которые код консьюмера совершает, поэтому не гниёт, как документ. Хорошие контракты матчат по типу и структуре, а не по точным значениям — защищая то, что консьюмер парсит, и молча о том, какое значение он получает, — и несут provider states, позволяющие провайдеру настроить предусловия, чтобы pact переигрывался детерминированно в изоляции. Затем pact публикуется в брокер, помеченный версией и веткой, готовый к верификации провайдером — что и есть следующий урок.

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

Trademarks belong to their respective owners. Editorial reference only.