Когда команда бэкенда переименовывает поле ответа, E2E-тесты в staging ловят это после деплоя. Контрактное тестирование ловит это до: фронтенд генерирует формальный контракт из своих тестов, а бэкенд проверяет что может удовлетворить эти ожидания независимо, без запуска всего интеграционного сьюта. Здесь разобран Pact для consumer-driven contract testing в TypeScript-стеке: от генерации pact-файла до запуска верификации провайдера в CI.

Что такое контрактное тестирование

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

Контракт описывает что именно consumer (фронтенд или другой сервис) ожидает от provider (API) в части формата ответов. Consumer генерирует контракт из своих тестов. Provider проверяет что может удовлетворить эти ожидания без запуска полного тест-сьюта consumer'а.

Результат: ломающие изменения API ловятся до того как попадают в staging.

Pact: стандартный инструмент

Pact: наиболее распространённая библиотека контрактного тестирования. Работает записывая реальные API-взаимодействия во время тестов consumer'а, сохраняя их как «pacts» (JSON-файлы), и проигрывая их против реального provider'а.

Установка на стороне consumer (фронтенд)

npm install -D @pact-foundation/pact

Написание consumer-теста

Этот тест определяет что фронтенд ожидает от эндпоинта /users/1:

import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import path from 'path';

const { like, string, integer } = MatchersV3;

const provider = new PactV3({
  consumer: 'frontend',
  provider: 'user-api',
  dir: path.resolve(process.cwd(), 'pacts'),
});

describe('User API contract', () => {
  it('returns user data', async () => {
    await provider
      .given('user 1 exists')
      .uponReceiving('a request for user 1')
      .withRequest({
        method: 'GET',
        path: '/users/1',
        headers: { Authorization: like('Bearer token123') },
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          id: integer(1),
          name: string('Alice'),
          email: string('alice@example.com'),
        },
      })
      .executeTest(async (mockServer) => {
        const response = await fetch(`${mockServer.url}/users/1`, {
          headers: { Authorization: 'Bearer token123' },
        });
        const data = await response.json();

        expect(data.name).toBeDefined();
        expect(data.email).toBeDefined();
      });
  });
});

При запуске этого теста Pact:

1. Запускает mock-сервер

2. Записывает взаимодействие (запрос + ожидаемый ответ) как pact-файл в ./pacts/

3. Запускает тест против mock-сервера

Сгенерированный pact-файл и есть контракт. Выглядит так:

{
  "consumer": { "name": "frontend" },
  "provider": { "name": "user-api" },
  "interactions": [{
    "description": "a request for user 1",
    "request": { "method": "GET", "path": "/users/1" },
    "response": {
      "status": 200,
      "body": { "id": 1, "name": "Alice", "email": "alice@example.com" }
    }
  }]
}

Верификация на стороне provider (бэкенд)

Команда бэкенда берёт этот pact-файл и проверяет что API его удовлетворяет:

import { Verifier } from '@pact-foundation/pact';

describe('Provider verification', () => {
  it('validates the expectations of Frontend', () => {
    return new Verifier({
      provider: 'user-api',
      providerBaseUrl: 'http://localhost:3001',
      pactUrls: [path.resolve(process.cwd(), 'pacts/frontend-user-api.json')],
      stateHandlers: {
        'user 1 exists': async () => {
          // засеиваем тестового пользователя в базу
          await db.users.create({ id: 1, name: 'Alice', email: 'alice@example.com' });
        },
      },
    }).verifyProvider();
  });
});

Верификатор отправляет записанный запрос к реальному бэкенду и проверяет что ответ соответствует контракту. Если команда бэкенда переименует name в full_name, эта верификация упадёт до того как какой-либо код попадёт в staging.

Где Playwright вписывается

Playwright обрабатывает E2E интеграционное тестирование. Контрактное тестирование обрабатывает верификацию на границах сервисов. Они дополняют друг друга.

На практике: Контрактные тесты ловят переименование полей, удалённые поля, изменённые статус-коды и несоответствия типов. E2E-тесты Playwright ловят проблемы с рендерингом UI, сбои пользовательских флоу и полные full-stack интеграционные сбои.

Не заменяй Playwright-тесты контрактными. Используй контрактные тесты чтобы сделать Playwright-тесты быстрее и стабильнее: меньше E2E-падений из-за изменений формата API которые должны были быть пойманы раньше.

Pact Broker: общий доступ к контрактам между командами

Когда consumer и provider находятся в отдельных репозиториях (типично для микросервисов), нужно центральное место для хранения и обмена pact-файлами. Pact Broker: стандартный инструмент, open source, для самостоятельного хостинга, или доступен как PactFlow (их облачный сервис).

Команды публикуют pacts после запуска consumer-тестов. Пайплайны provider'а забирают последний pact и верифицируют перед деплоем. Это создаёт проверку зависимостей: provider не может деплоиться если ломает контракт consumer'а.

Когда контрактное тестирование оправдано

Контрактное тестирование добавляет накладные расходы: написание consumer-тестов на DSL Pact, настройка обработчиков состояний provider'а, поддержка broker'а. Оно окупается когда:

  • Несколько фронтенд-приложений или сервисов вызывают одно API
  • Изменения API часто ломают downstream consumer'ов
  • Архитектура микросервисов где команды деплоятся независимо
  • E2E-сьют медленный и хочется ловить интеграционные сбои раньше

Вероятно не оправдано для монолита где фронтенд и бэкенд в одном репозитории и деплоятся вместе, команды из двух человек где обе стороны один и тот же человек, и раннего проекта где API меняется слишком часто чтобы поддерживать стабильные контракты.

Начни с Playwright для E2E. Добавляй контрактное тестирование когда есть два независимо деплоящихся сервиса которые ломают предположения друг друга.

→ See also: API-тестирование с Playwright APIRequestContext (без Postman) | API-тестирование 101: всё, что нужно знать QA-инженеру в 2026 году | Пирамида тестирования: объяснение для QA-инженеров