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