Kravchenko

Web Lab

АудитБлогКонтакты

Kravchenko

Web Lab

Разрабатываем сайты и автоматизацию на современных фреймворках под ключ

Услуги
ЛендингМногостраничныйВизитка
E-commerceБронированиеПортфолио
Навигация
БлогКонтактыАудит
Обратная связь
+7 921 567-11-16
info@kravlab.ru
с 09:00 до 18:00

© 2026 Все права защищены

•

ИП Кравченко Никита Владимирович

•

ОГРНИП: 324784700339743

Политика конфиденциальности

Consumer‑driven контрактные тесты с Pact: интеграции без срывов релизов и меньше согласований

Разработка и технологии16 марта 2026 г.
Когда продукт зависит от сторонних сервисов, интеграции часто ломаются в самый неподходящий момент. Consumer‑driven контрактные тесты с Pact позволяют согласовать ожидания потребителя и фактическое поведение поставщика до релиза — без бесконечных переписок и ручных прогонов. Результат — быстрее релизы, меньше инцидентов и предсказуемые интеграции.
Consumer‑driven контрактные тесты с Pact: интеграции без срывов релизов и меньше согласований

• Оглавление

  • Зачем бизнесу контрактные тесты
  • Как работает consumer‑driven подход
  • Минимальный пример на Node.js: потребитель и поставщик
  • Публикация контрактов в Pact Broker
  • Контрактные тесты в CI: пример GitHub Actions
  • Как дружить Pact и OpenAPI
  • Практика и грабли: версии, тестовые данные, стабильность
  • Экономический эффект: где деньги
  • Чек‑лист внедрения

Зачем бизнесу контрактные тесты

Интеграции ломаются не потому, что разработчики «плохие», а потому что стороны видят контракт по‑разному. Поставщик меняет поле или формат ответа — потребитель падает в проде. Полные интеграционные и end‑to‑end тесты помогают, но они дорогие, медленные и хрупкие.

Consumer‑driven контрактные тесты (далее — CTC) решают проблему на уровне «ожидания → доказательство → верификация»:

  • Потребитель формализует ожидания в виде контракта.
  • Контракт автоматически проверяется на стороне поставщика до релиза.
  • Результаты верификации и совместимости версий хранятся в брокере.

Бизнес‑выгода:

  • Меньше срывов релизов и откатов.
  • Независимые релизы команд (снижение «координационных» затрат).
  • Быстрый разбор инцидентов (видно, какая версия кого ломает).
  • Прозрачная совместимость по веткам и средам.

Как работает consumer‑driven подход

Ключевые роли:

  • Потребитель (consumer) — сервис, который вызывает API другого сервиса.
  • Поставщик (provider) — сервис, который реализует API.
  • Pact Broker — хранилище контрактов, статусов проверок и правил совместимости.

Поток:

  1. Потребитель запускает тесты с локальным мок‑сервером Pact, описывает ожидания и генерирует файл контракта (pact).
  2. Контракт публикуется в Pact Broker.
  3. Поставщик забирает контракты из брокера и верифицирует: поднимает свой API и прогоняет проверки.
  4. Результат верификации публикуется обратно в брокер. Статус виден всем.

Важно: контракт не описывает ВСЁ поведение API, а фиксирует то, что реально нужно потребителю. Это делает контракты компактными, стабильными и дешевыми в сопровождении.

Минимальный пример на Node.js: потребитель и поставщик

Сценарий: сервис «Витрина» (consumer) читает карточку товара у «Каталога» (provider).

Структура проекта

.
├─ package.json
├─ jest.config.js
├─ src
│  ├─ provider.js
│  └─ server.js
├─ test
│  ├─ consumer.pact.test.js
│  └─ provider.pact.verify.test.js
└─ pacts/   # сюда будут генерироваться контракты

package.json

{
  "name": "pact-demo",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "start:provider": "node src/server.js",
    "test:consumer": "jest test/consumer.pact.test.js --runInBand",
    "test:provider": "jest test/provider.pact.verify.test.js --runInBand",
    "pact:publish": "docker run --rm -v $PWD/pacts:/pacts pactfoundation/pact-cli:latest pact-broker publish /pacts --consumer-app-version $(git rev-parse --short HEAD) --branch main --broker-base-url http://localhost:9292"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "express": "^4.18.3"
  },
  "devDependencies": {
    "@pact-foundation/pact": "^12.0.0",
    "jest": "^29.7.0"
  }
}

jest.config.js

module.exports = {
  testEnvironment: 'node',
  verbose: true
};

Поставщик: простой Express‑сервис

// src/provider.js
const express = require('express');

const app = express();
app.use(express.json());

let db = new Map();

function reset() {
  db = new Map();
}

function seedProduct(product) {
  db.set(String(product.id), product);
}

app.get('/products/:id', (req, res) => {
  const id = String(req.params.id);
  const product = db.get(id);
  if (!product) {
    return res.status(404).json({ message: 'not_found' });
  }
  res.json(product);
});

module.exports = { app, reset, seedProduct };
// src/server.js
const { app } = require('./provider');

const port = process.env.PORT || 9000;
app.listen(port, () => console.log(`Provider listening on :${port}`));

Потребитель: контрактные тесты (генерация pact)

// test/consumer.pact.test.js
const path = require('path');
const axios = require('axios');
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');

const provider = new PactV3({
  consumer: 'webshop',
  provider: 'catalog',
  dir: path.resolve(process.cwd(), 'pacts')
});

describe('Webshop -> Catalog contract', () => {
  test('должен получить карточку товара по ID', async () => {
    provider
      .given('product with ID 42 exists')
      .uponReceiving('a request for existing product 42')
      .withRequest({
        method: 'GET',
        path: '/products/42'
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json; charset=utf-8' },
        body: MatchersV3.like({
          id: 42,
          name: MatchersV3.string('Кружка «Лого»'),
          price: MatchersV3.integer(49900),
          currency: MatchersV3.string('RUB')
        })
      });

    await provider.executeTest(async (mockServer) => {
      const res = await axios.get(`${mockServer.url}/products/42`);
      expect(res.status).toBe(200);
      expect(res.data).toEqual(
        expect.objectContaining({ id: 42, currency: 'RUB' })
      );
    });
  });

  test('должен получить 404 для несуществующего товара', async () => {
    provider
      .given('product 999 does not exist')
      .uponReceiving('a request for a missing product 999')
      .withRequest({ method: 'GET', path: '/products/999' })
      .willRespondWith({
        status: 404,
        headers: { 'Content-Type': 'application/json; charset=utf-8' },
        body: MatchersV3.like({ message: 'not_found' })
      });

    await provider.executeTest(async (mockServer) => {
      await expect(axios.get(`${mockServer.url}/products/999`))
        .rejects.toHaveProperty('response.status', 404);
    });
  });
});

Контракт будет сгенерирован в каталоге pacts как файл webshop-catalog.json.

Верификация на стороне поставщика

// test/provider.pact.verify.test.js
const path = require('path');
const { Verifier } = require('@pact-foundation/pact');
const { app, reset, seedProduct } = require('../src/provider');

let server;

beforeAll(async () => {
  await new Promise((resolve) => {
    server = app.listen(9000, () => resolve());
  });
});

afterAll(async () => {
  await new Promise((resolve) => server.close(() => resolve()));
});

/**
 * Локальная верификация по файлу контракта.
 * Для работы с Broker см. раздел ниже.
 */

test('верификация контрактов потребителей', async () => {
  reset();

  const verifier = new Verifier({
    providerBaseUrl: 'http://localhost:9000',
    pactUrls: [path.resolve(process.cwd(), 'pacts/webshop-catalog.json')],
    stateHandlers: {
      'product with ID 42 exists': async () => {
        seedProduct({ id: 42, name: 'Кружка «Лого»', price: 49900, currency: 'RUB' });
      },
      'product 999 does not exist': async () => {
        reset();
      }
    }
  });

  const output = await verifier.verifyProvider();
  console.log(output);
});

Публикация контрактов в Pact Broker

Хранить контракты в Git — можно, но вы теряете историю проверок и совместимость версий. Pact Broker решает это «из коробки».

Docker Compose для брокера

# docker-compose.yml
version: '3.8'
services:
  db:
    image: postgres:15
    environment:
      POSTGRES_USER: pactbroker
      POSTGRES_PASSWORD: pactbroker
      POSTGRES_DB: pactbroker
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "pactbroker"]
      interval: 5s
      timeout: 5s
      retries: 10
  broker:
    image: pactfoundation/pact-broker:latest
    ports:
      - "9292:9292"
    environment:
      PACT_BROKER_DATABASE_URL: postgres://pactbroker:pactbroker@db:5432/pactbroker
      PACT_BROKER_LOG_LEVEL: INFO
    depends_on:
      db:
        condition: service_healthy

Запуск:

docker compose up -d

Публикация контракта из проекта потребителя:

npm run test:consumer && npm run pact:publish

Верификация у поставщика через Broker (публикуем статусы):

// пример verifier с Broker (замените в provider.pact.verify.test.js)
const verifier = new Verifier({
  providerBaseUrl: 'http://localhost:9000',
  provider: 'catalog',
  pactBrokerUrl: 'http://localhost:9292',
  publishVerificationResult: true,
  providerVersion: process.env.GIT_SHA || 'dev-local',
  providerVersionBranch: process.env.GIT_BRANCH || 'local',
  consumerVersionSelectors: [
    { branch: 'main', latest: true }
  ],
  stateHandlers: {
    'product with ID 42 exists': async () => {
      seedProduct({ id: 42, name: 'Кружка «Лого»', price: 49900, currency: 'RUB' });
    },
    'product 999 does not exist': async () => {
      reset();
    }
  }
});

Контрактные тесты в CI: пример GitHub Actions

Два джоба: один генерирует и публикует контракт (consumer), второй поднимает поставщика и верифицирует.

# .github/workflows/contracts.yml
name: Contracts

on:
  push:
    branches: [ "main" ]

jobs:
  consumer:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: docker compose -f docker-compose.yml up -d broker db
      - run: npm run test:consumer
      - run: npm run pact:publish

  provider:
    runs-on: ubuntu-latest
    needs: consumer
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: docker compose -f docker-compose.yml up -d broker db
      - name: Верификация от Broker
        env:
          GIT_SHA: ${{ github.sha }}
          GIT_BRANCH: ${{ github.ref_name }}
        run: |
          node -e "require('child_process').spawnSync('node',['-e','console.log(\'start\')'])"
          npm run test:provider

Под реальный проект имеет смысл разделить репозитории/пайплайны потребителя и поставщика и добавить правила: не релизим, если в Broker есть неподтвержденные (pending) контракты.

Как дружить Pact и OpenAPI

Открытый вопрос: если у нас есть OpenAPI, зачем Pact? Ответ: у них разные задачи.

  • OpenAPI — описание «как должно быть» (документация и генерация клиентов/валидаторов).
  • Pact — подтверждение «как используется» конкретными потребителями (фактические ожидания и обратная совместимость).

Практика:

  • Храните OpenAPI рядом с кодом поставщика и проверяйте его в CI (линтер, генерация клиента).
  • Контракты Pact публикуйте в Broker и используйте селекторы веток/версий.
  • Не пытайтесь покрыть Pact‑контрактами все поля схемы. Опишите минимально необходимое потребителю.

Мини‑пример OpenAPI под наш endpoint:

openapi: 3.0.3
info:
  title: Catalog API
  version: 1.0.0
paths:
  /products/{id}:
    get:
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [id, name, price, currency]
                properties:
                  id: { type: integer }
                  name: { type: string }
                  price: { type: integer, minimum: 0 }
                  currency: { type: string, minLength: 3, maxLength: 3 }
        '404':
          description: Not Found
          content:
            application/json:
              schema:
                type: object
                required: [message]
                properties:
                  message: { type: string, enum: [not_found] }

Совет: добавьте в поставщика валидацию запросов/ответов по OpenAPI (например, express-openapi-validator). Так вы гарантируете, что реализованный API соответствует спецификации, а Pact гарантирует, что изменения не ломают потребителей.

Практика и грабли: версии, тестовые данные, стабильность

  • Тестовые данные через stateHandlers. Не жёстко хардкодьте реальные ID; готовьте состояние поставщика перед верификацией.
  • Используйте matchers (like, integer, string) — не привязывайтесь к конкретным значениям и форматам времени.
  • Теги/ветки. Помечайте публикации ветками (branch) и окружениями (например, tag prod). Это позволит проверять совместимость нужных версий.
  • Малые контракты. Опишите только используемые поля. «Толстые» контракты чаще ломают обратную совместимость.
  • Переименование полей — ломающие изменения. Сначала добавляйте новые поля, поддерживайте оба варианта, затем объявляйте деприкацию и убирайте старые после подтверждения всех потребителей.
  • Не подменяйте Pact‑ом всю проверку. Он не заменяет сквозные тесты и нагрузку. Это слой «контракта», а не «бизнес‑флоу целиком».
  • Логи. Пробрасывайте trace‑id в заголовках и логах при верификации — разбор ошибок станет проще.

Экономический эффект: где деньги

Опираясь на опыт внедрения у продуктовых команд:

  • Снижение интеграционных инцидентов на 30–60% за 2–3 месяца.
  • Сокращение времени согласований изменений с недель до дней — контракт виден в Broker, не нужно «переписок».
  • MTTR ниже на 20–40%: видно, какая версия/ветка несовместима, и кто блокирует релиз.
  • Параллельные релизы: команда поставщика не ждёт потребителей, если изменения обратносуместимы и проверены.

Чек‑лист внедрения

  • Определите критичные интеграции (2–3 для начала).
  • Настройте Pact Broker (или используйте управляемый сервис).
  • Опишите минимальные контракты на стороне потребителя. Используйте matchers.
  • Поднимите верификацию у поставщика. Добавьте stateHandlers и фикстуры.
  • Включите публикацию/верификацию в CI. Блокируйте релиз при «красных» статусах.
  • Сведите Pact и OpenAPI: автоматическая валидация ответов по схеме.
  • Обучите команды правилам обратной совместимости и версии/ветки в Broker.

Итог: consumer‑driven контрактные тесты не пытаются заменить интеграционные испытания, а делают их адресными и предсказуемыми. Это быстрый и недорогой способ стабилизировать интеграции, ускорить релизы и убрать «неожиданные» поломки на проде — там, где они особенно дороги.


интеграцииконтрактные тестыPact