Kravchenko

Web Lab

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

Kravchenko

Web Lab

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

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

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

•

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

•

ОГРНИП: 324784700339743

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

Контрактные тесты (CDC) между сервисами: меньше интеграционных багов и быстрее релизы

Разработка и технологии12 января 2026 г.
Контрактные тесты позволяют согласовать ожидания между сервисами до релиза — без тяжёлых интеграционных стендов и ручной возни. В итоге меньше инцидентов на продакшене, быстрее согласования между командами и более предсказуемые релизы.
Контрактные тесты (CDC) между сервисами: меньше интеграционных багов и быстрее релизы

Оглавление

  • Зачем бизнесу контрактные тесты
  • Что такое CDC и чем это лучше интеграционных стендов
  • Минимальный пример на Pact: потребитель, провайдер, проверка
    • Запускаем локальный Pact Broker (для публикации контрактов)
    • Потребитель: клиент и контрактный тест
    • Провайдер: API и верификация контрактов
  • CI/CD: publish/verify, can-i-deploy и запрет релиза при несовместимости
  • Эволюция контрактов без поломок
  • Реалистичные сценарии и состояния провайдера
  • Типичные ошибки и как их избежать
  • Когда CDC не нужны или избыточны
  • План внедрения за 4 недели
  • Метрики успеха
  • Инструменты и ссылки

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

Интеграционные баги между сервисами — одни из самых дорогих. Они часто всплывают поздно: на стейджинге или уже на продакшене, когда изменения из нескольких команд наконец встречаются. Контрактные тесты переносят проверку совместимости на этап коммитов: команды согласуют «контракт» (что, как и в каком формате сервисы передают друг другу) до релиза.

Бизнес‑эффект:

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

Что такое CDC и чем это лучше интеграционных стендов

CDC (Consumer‑Driven Contracts) — «контракты, управляемые потребителем». Потребитель сервиса описывает свои ожидания к ответам провайдера и публикует их в виде контракта. Провайдер регулярно проверяет, что реализация этим ожиданиям соответствует.

Как это работает на практике:

  1. Потребитель формулирует примеры запросов/ответов, которые ему нужны, и запускает эти примеры против локального мок‑сервера.
  2. По итогам теста генерируется файл контракта.
  3. Контракт публикуется в репозиторий контрактов (например, Pact Broker).
  4. Провайдер в своём пайплайне забирает опубликованные контракты и проверяет их против реального приложения (в локальном окружении или контейнере) — с настройкой данных под нужные «состояния».
  5. Если проверка красная, релиз блокируется.

Чем это отличается от интеграционных стендов:

  • Нужны не «все со всеми» живые сервисы, а только контракт и текущая версия провайдера.
  • Проверки быстрые и детерминированные: без хрупких зависимостей и гонок.
  • Легче масштабировать: добавление нового потребителя — это новый контракт, а не новый стенд.

Минимальный пример на Pact: потребитель, провайдер, проверка

Ниже — рабочий базовый пример на Node.js с Pact Foundation. Потребитель — клиент, который запрашивает профиль пользователя. Провайдер — API, отдающий этот профиль.

Запускаем локальный Pact Broker (для публикации контрактов)

version: "3.9"
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: pact
      POSTGRES_PASSWORD: pact
      POSTGRES_DB: pact
    ports:
      - "5433:5432"
  pact-broker:
    image: pactfoundation/pact-broker:2.122.0.0
    depends_on:
      - postgres
    ports:
      - "9292:9292"
    environment:
      PACT_BROKER_DATABASE_URL: postgres://pact:pact@postgres:5432/pact
      PACT_BROKER_LOG_LEVEL: INFO
      PACT_BROKER_PUBLIC_HEARTBEAT: "true"

Сохраните как docker-compose.yml и запустите: docker compose up -d.

Потребитель: клиент и контрактный тест

Структура:

  • consumer/
    • package.json
    • src/client.js
    • test/consumer.pact.test.js
{
  "name": "consumer",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "test": "node --test",
    "pact:publish": "pact-broker publish pacts --consumer-app-version=$(git rev-parse --short HEAD) --broker-base-url=http://localhost:9292"
  },
  "dependencies": {
    "node-fetch": "3.3.2"
  },
  "devDependencies": {
    "@pact-foundation/pact": "12.3.0",
    "@pact-foundation/pact-core": "13.6.0"
  }
}
// consumer/src/client.js
import fetch from 'node-fetch';

export async function getUser(baseUrl, id) {
  const res = await fetch(`${baseUrl}/users/${id}`, {
    headers: { 'Accept': 'application/json' }
  });
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }
  const data = await res.json();
  // Потребителю важно: число id, обязательные email и name
  return {
    id: Number(data.id),
    email: data.email,
    name: data.name,
    // Остальные поля не критичны — игнорируем
  };
}
// consumer/test/consumer.pact.test.js
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { getUser } from '../src/client.js';
import assert from 'node:assert/strict';

const { like, regex, integer } = MatchersV3;

const pact = new PactV3({
  consumer: 'web-frontend',
  provider: 'user-service'
});

await pact.addInteraction()
  .given('User 42 exists') // состояние провайдера
  .uponReceiving('get user by id')
  .withRequest({
    method: 'GET',
    path: regex({ generate: '/users/42', matcher: '/users/\\d+' }),
    headers: { 'Accept': 'application/json' }
  })
  .willRespondWith({
    status: 200,
    headers: { 'Content-Type': 'application/json; charset=utf-8' },
    body: {
      id: integer(42),
      email: regex({ generate: 'user42@example.com', matcher: '.+@.+\\..+' }),
      name: like('Ada Lovelace'),
      // Допустимы дополнительные поля — провайдер может их добавлять
    }
  });

await pact.executeTest(async (mock) => {
  const result = await getUser(mock.url, 42);
  assert.equal(result.id, 42);
  assert.match(result.email, /@/);
});

Тест запустит мок‑провайдера Pact, проверит ожидания и сгенерирует контракт pacts/web-frontend-user-service.json.

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

npm --prefix consumer test
npm --prefix consumer run pact:publish

Провайдер: API и верификация контрактов

Структура:

  • provider/
    • package.json
    • src/server.js
    • test/verify.pact.test.js
{
  "name": "provider",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node src/server.js",
    "verify": "node test/verify.pact.test.js"
  },
  "dependencies": {
    "express": "4.19.2"
  },
  "devDependencies": {
    "@pact-foundation/pact": "12.3.0"
  }
}
// provider/src/server.js
import express from 'express';

export function createApp({ repo }) {
  const app = express();

  app.get('/users/:id', (req, res) => {
    const user = repo.getUser(Number(req.params.id));
    if (!user) return res.status(404).json({ error: 'not_found' });
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    res.json(user);
  });

  return app;
}

if (process.env.NODE_ENV !== 'test') {
  // Пример запуска в одиночку
  const app = createApp({
    repo: {
      getUser: (id) => ({ id, email: `user${id}@example.com`, name: 'Ada Lovelace' })
    }
  });
  const port = process.env.PORT || 3000;
  app.listen(port, () => console.log(`user-service on ${port}`));
}
// provider/test/verify.pact.test.js
import { Verifier } from '@pact-foundation/pact';
import { createApp } from '../src/server.js';

// Фиктивное хранилище, подстраиваемое под состояния
const repo = {
  getUser: (id) => null
};

// Сервер для проверки (порт выбираем заранее)
const port = 4001;
const app = createApp({ repo });
const server = app.listen(port);

async function run() {
  const verifier = new Verifier({
    providerBaseUrl: `http://localhost:${port}`,
    provider: 'user-service',
    providerVersion: process.env.GIT_COMMIT || 'dev',
    pactBrokerUrl: 'http://localhost:9292',
    // Проверяем только контракты потребителя web-frontend
    consumers: ['web-frontend'],
    stateHandlers: {
      'User 42 exists': async () => {
        repo.getUser = (id) => id === 42 ? ({ id, email: 'user42@example.com', name: 'Ada Lovelace' }) : null;
      }
    }
  });

  try {
    const output = await verifier.verifyContract();
    console.log('Pact verification complete:', output);
    process.exit(0);
  } catch (e) {
    console.error('Pact verification failed:', e);
    process.exit(1);
  } finally {
    server.close();
  }
}

run();

Если провайдер не соответствует ожиданиям, верификация упадёт с понятным сообщением: что именно не совпало и в каком поле.

CI/CD: publish/verify, can-i-deploy и запрет релиза при несовместимости

Типичный пайплайн:

  • Потребитель: прогоняет контрактные тесты → публикует контракт в Broker → тэгирует версию (например, feat-x) → опционально запускает can-i-deploy, чтобы понять, можно ли выкатывать потребителя с текущими провайдерами.
  • Провайдер: поднимает приложение → подтягивает актуальные контракты из Broker → верифицирует → публикует результат → запускает can-i-deploy для своего релиза.

Примеры GitHub Actions:

# .github/workflows/consumer.yml
name: consumer
on: [push]
jobs:
  test-and-publish:
    runs-on: ubuntu-latest
    services:
      broker:
        image: pactfoundation/pact-broker:2.122.0.0
        ports: ["9292:9292"]
        env:
          PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact.db
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci --prefix consumer
      - run: npm --prefix consumer test
      - run: npm --prefix consumer run pact:publish
# .github/workflows/provider.yml
name: provider
on: [push]
jobs:
  verify:
    runs-on: ubuntu-latest
    services:
      broker:
        image: pactfoundation/pact-broker:2.122.0.0
        ports: ["9292:9292"]
        env:
          PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact.db
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci --prefix provider
      - run: npm --prefix provider run verify

Для продакшен‑цепочки добавьте pact-broker can-i-deploy с тегами окружений (например, --to-environment=prod) и блокируйте релиз при несовместимости. Так вы исключите «ломающие» выкаты ещё до попадания в прод.

Эволюция контрактов без поломок

Как безопасно менять API:

  • Добавляйте поля, не удаляйте сразу. Новые поля — необязательные для потребителей. Контракты потребителей должны игнорировать лишнее.
  • Для удаления: сначала пометьте поле как устаревшее, дайте срок и уведомления, проверьте, что ни один потребитель это поле не матчит как обязательное. Только потом удаляйте.
  • Меняете формат — вводите новое поле или версию ресурса (например, новый путь или заголовок версии). Поддерживайте обе версии до миграции потребителей.
  • Стабильные типы: даты — ISO 8601 в UTC; числа — без «строкизации»; явные единицы измерения и валюты.

Pact/Broker помогает с матрицей совместимости: видно, какие версии потребителей прошли проверку против каких версий провайдера и можно ли выкатывать.

Реалистичные сценарии и состояния провайдера

Сами по себе контракты — это примеры. Сделайте их репрезентативными:

  • Используйте «состояния» провайдера (Provider States), чтобы описывать преднастройку данных: «пользователь существует», «баланс нулевой», «нет прав».
  • Добавляйте негативные сценарии: 404, 401/403, 409 — это то, что чаще всего ломает клиентскую логику.
  • Включайте заголовки и коды ошибок: «Content-Type», «X-Request-Id», «Retry-After» и т. п., если клиент от них зависит.

Типичные ошибки и как их избежать

  • Переспецификация. Потребитель требует точный набор полей и порядок, хотя на деле ему достаточно пары ключевых атрибутов. Решение: используйте матчеры (например, «строка, похожая на email»), игнорируйте лишние поля.
  • Хрупкие значения. Тесты привязаны к конкретной дате/времени, ID или локали. Решение: матчеры для форматов, а не конкретных значений; генерация предсказуемых данных.
  • Неучтённые состояния. Проверили только «счастливый путь». Решение: добавьте 3–5 ключевых негативных сценариев по каждому критичному эндпойнту.
  • Игнор заголовков. У клиента «application/json», у сервиса — «text/json» или другая версия — и всё падает. Решение: явно фиксируйте заголовки в контракте.
  • Большие тела. Контракты превращаются в дампы гигантских объектов. Решение: проверяйте только часть, нужную потребителю.
  • Отсутствие политики версий. Команды удаляют поля без миграции потребителей. Решение: договоритесь о правилах эволюции и графике деприкаций, используйте матрицу совместимости и can‑i‑deploy.

Когда CDC не нужны или избыточны

  • Один монолит и общий типовой слой — быстрее написать модульные тесты и покрыть доменные инварианты.
  • Чистая договорённость по схеме (например, строгое JSON Schema/Protobuf + статическая генерация клиентов) может быть достаточной, если нет динамических состояний и сложной логики.
  • Внешние SaaS/партнёрские API, на которые вы не влияете: тут лучше контракт фиксировать как «фриз» (зеркалировать документацию) и мокать ответы у себя. Полноценный CDC может быть избыточным.

План внедрения за 4 недели

  • Неделя 1. Пилот на одном потребителе и одном провайдере, 2–3 эндпойнта, минимальная матрица.
  • Неделя 2. Добавьте негативные сценарии, заведите Pact Broker, подключите публикацию и верификацию в CI.
  • Неделя 3. Введите правило: ни один релиз провайдера не проходит, если верификация контрактов красная. Включите can‑i‑deploy для критичных сервисов.
  • Неделя 4. Расширьте покрытие до 60–80% интеграционных взаимодействий, договоритесь о политике версий и деприкаций.

Метрики успеха

  • Снижение доли инцидентов класса «несовместимость API» и откатов релизов.
  • Ускорение «коммит → прод» для изменений в контрактных сервисах.
  • Доля релизов, прошедших can‑i‑deploy с первого раза.
  • Время поиска причин интеграционных багов: с часами на стенде до минут в отчёте верификатора.

Инструменты и ссылки

  • Pact Foundation (JS/TS, JVM, Go, Ruby, .NET) — де‑факто стандарт, Broker и can‑i‑deploy.
  • Spring Cloud Contract — альтернатива для экосистемы JVM с генерацией тестов.
  • Specmatic — контракты на основе OpenAPI/AsyncAPI.
  • JSON Schema/Protobuf + линтеры — полезны как дополнение, фиксируют структуру и типы.

Контрактные тесты — это «страховка» между командами. Они дешевле стендов, понятнее логов из продакшена и позволяют выпускать чаще, не боясь сломать соседей. Начните с малого пилота — и через месяц вы уже не захотите возвращаться к прежним «интеграционным лотереям».


контрактные тестымикросервисыкачество