Contract Testing

Contract Testing — подход к тестированию взаимодействия между сервисами, при котором потребитель (consumer) и поставщик (provider) независимо проверяют соответствие согласованному контракту формата данных.

Зачем нужно

В микросервисной архитектуре E2E тесты всей системы медленные и ненадёжные. Contract Testing позволяет каждой команде проверять свой сервис изолированно: потребитель проверяет, что умеет работать с ответом поставщика, а поставщик — что его API соответствует ожиданиям потребителей. Это ловит breaking changes до деплоя.

Где используется

  • Микросервисы: проверка совместимости между сервисами при независимых деплоях
  • Frontend + Backend: фронтенд проверяет, что API вернёт нужные поля
  • Публичные API: поставщик убеждается, что не сломал клиентов при изменении схемы

Основной контент

Consumer-Driven Contract Testing (CDCT)

Самый распространённый подход: потребитель диктует контракт.

Consumer → пишет тест с ожидаемым форматом ответа
         → генерирует файл контракта (pact)
         → публикует в Pact Broker

Provider → скачивает контракт из Pact Broker
         → проверяет свой API против контракта
         → публикует результат верификации

Пример с Pact.js (consumer side)

// userService.consumer.test.js
import { PactV3, MatchersV3 } from '@pact-foundation/pact';

const { like, string } = MatchersV3;

const provider = new PactV3({
  consumer: 'FrontendApp',
  provider: 'UserService',
  dir: './pacts',
});

test('получает пользователя по id', async () => {
  await provider
    .addInteraction({
      states: [{ description: 'пользователь с id=1 существует' }],
      uponReceiving: 'GET /users/1',
      withRequest: { method: 'GET', path: '/users/1' },
      willRespondWith: {
        status: 200,
        body: {
          id: like(1),
          name: string('Alice'),
          email: string('alice@example.com'),
        },
      },
    })
    .executeTest(async (mockProvider) => {
      const client = new UserApiClient(mockProvider.url);
      const user = await client.getUser(1);
      expect(user.name).toBe('Alice');
    });
});

Пример с Pact.js (provider side)

// userService.provider.test.js
import { PactV3 } from '@pact-foundation/pact';

const verifier = new PactV3({
  provider: 'UserService',
  providerBaseUrl: 'http://localhost:3000',
  pactBrokerUrl: 'https://pact-broker.example.com',
  publishVerificationResult: true,
  providerVersion: '1.2.3',
});

test('проверяет контракт с FrontendApp', async () => {
  await verifier.verifyProvider;
});

Альтернатива: схемный контракт (JSON Schema)

// Простой вариант без Pact — валидация схемой
import Ajv from 'ajv';

const userSchema = {
  type: 'object',
  required: ['id', 'name', 'email'],
  properties: {
    id: { type: 'number' },
    name: { type: 'string' },
    email: { type: 'string', format: 'email' },
  },
};

test('ответ API соответствует контракту', async () => {
  const user = await fetchUser(1);
  const ajv = new Ajv();
  const valid = ajv.validate(userSchema, user);
  expect(valid).toBe(true);
});

Частые ошибки

  • Contract test как integration test — contract test изолирован; не нужно поднимать реальный провайдер на стороне потребителя
  • Слишком жёсткий контракт — точное совпадение всех полей ломается при добавлении новых полей в API; используй like для гибкости
  • Нет Pact Broker — без централизованного хранилища контрактов консьюмер и провайдер не могут обменяться артефактами в CI

Связанные темы

Ресурсы