Контрактное тестирование с Pact, управляемое потребителем
Updated Jul 2026
Проблема, которую решает Pact
В микросервисной архитектуре сервис A вызывает сервис B. Если B изменит свой API, A сломается. Но собственные тесты B проходят, потому что B не знает об ожиданиях A. Контракты, управляемые потребителем переворачивают подход: потребитель (A) публикует контракт, описывающий его ожидания, а провайдер (B) проверяет, что может выполнить этот контракт.
Рабочий процесс Pact
+--------------+ +--------------+ +--------------+
| CONSUMER | | PACT BROKER | | PROVIDER |
| (Service A) | | (Registry) | | (Service B) |
| | | | | |
| 1. Write |--pact-->| 2. Store |--pact-->| 3. Verify |
| consumer | file | contract | file | provider |
| test | | | | test |
+--------------+ +--------------+ +--------------+
Шаг 1: Потребитель пишет тесты
Команда потребителя пишет тесты, описывающие ожидания от провайдера. Эти тесты выполняются против мок-сервера Pact, а не реального провайдера.
Шаг 2: Pact Broker хранит контракты
Сгенерированный Pact-файл (JSON-контракт) публикуется в центральный брокер. Брокер хранит все контракты потребителей и отслеживает совместимость версий.
Шаг 3: Провайдер верифицирует
Команда провайдера запускает верификационные тесты, воспроизводящие ожидания потребителя на реальной реализации провайдера. Если любое ожидание не выполняется, провайдер знает, что он сломает потребителя.
Почему Pact важен для тестирования с ИИ
Традиционный рабочий процесс Pact требует ручного написания контрактов -- утомительный процесс, который команды часто пропускают. ИИ меняет это:
- Анализ клиентского кода для автоматического определения паттернов вызовов API
- Генерация потребительских контрактов из реального использования, а не документации
- Обнаружение дрейфа контрактов при изменениях провайдера
- Предложение минимально совместимых контрактов при возникновении конфликтов
Анатомия контракта Pact
{
"consumer": { "name": "StorefrontUI" },
"provider": { "name": "ProductService" },
"interactions": [
{
"description": "a request for product ABC-123",
"providerState": "product ABC-123 exists",
"request": {
"method": "GET",
"path": "/api/v2/products/ABC-123",
"headers": {
"Authorization": "Bearer valid-token"
}
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"body": {
"id": "ABC-123",
"name": "Blue Widget",
"price": 29.99,
"category": "electronics",
"in_stock": true
},
"matchingRules": {
"body": {
"$.id": { "matchers": [{ "match": "type" }] },
"$.name": { "matchers": [{ "match": "type" }] },
"$.price": { "matchers": [{ "match": "decimal" }] },
"$.category": { "matchers": [{ "match": "regex", "regex": "electronics|clothing|food|other" }] }
}
}
}
}
]
}
Ключевые концепции
Provider States: Предусловия, которые должны быть истинными для взаимодействия. «product ABC-123 exists» означает, что верификационный тест провайдера должен настроить это состояние перед воспроизведением запроса.
Matching Rules: Вместо точного сопоставления значений Pact использует гибкие матчеры:
type-- значение должно быть того же типа (string, number, boolean)regex-- значение должно совпадать с паттерномdecimal-- значение должно быть десятичным числомlike-- структура должна совпадать (те же ключи, совместимые типы)eachLike-- массив, где каждый элемент совпадает с примером
Эта гибкость критически важна: потребителю неважно, что имя продукта -- именно «Blue Widget», ему важно только то, что поле name существует и является строкой.
Написание тестов потребителя
JavaScript/TypeScript с Pact V4
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike, uuid, decimal, term } = MatchersV3;
const provider = new PactV4({
consumer: 'StorefrontUI',
provider: 'ProductService',
});
describe('ProductClient Pact Tests', () => {
describe('getProduct', () => {
it('returns a product when it exists', async () => {
await provider
.addInteraction()
.given('product ABC-123 exists')
.uponReceiving('a request for product ABC-123')
.withRequest('GET', '/api/v2/products/ABC-123', (builder) => {
builder.headers({ Authorization: like('Bearer valid-token') });
})
.willRespondWith(200, (builder) => {
builder.jsonBody({
id: uuid(),
name: like('Blue Widget'),
price: decimal(29.99),
category: term({
generate: 'electronics',
regex: 'electronics|clothing|food|other'
}),
in_stock: like(true),
});
})
.executeTest(async (mockServer) => {
const client = new ProductClient(mockServer.url, 'valid-token');
const product = await client.getProduct('ABC-123');
expect(product.name).toBeDefined();
expect(product.price).toBeGreaterThanOrEqual(0);
});
});
it('returns 404 when product does not exist', async () => {
await provider
.addInteraction()
.given('product NONEXISTENT does not exist')
.uponReceiving('a request for a nonexistent product')
.withRequest('GET', '/api/v2/products/NONEXISTENT', (builder) => {
builder.headers({ Authorization: like('Bearer valid-token') });
})
.willRespondWith(404)
.executeTest(async (mockServer) => {
const client = new ProductClient(mockServer.url, 'valid-token');
await expect(client.getProduct('NONEXISTENT'))
.rejects.toThrow();
});
});
});
});
Python с Pact
from pact import Consumer, Provider
pact = Consumer('InventoryDashboard').has_pact_with(
Provider('ProductService'),
pact_dir='./pacts'
)
def test_get_product():
expected_body = {
"id": Term(r'^[a-f0-9-]{36}$', "550e8400-e29b-41d4-a716-446655440000"),
"name": Like("Blue Widget"),
"price": Like(29.99),
"category": Term(r'^(electronics|clothing|food|other)$', "electronics"),
}
(pact
.given("product exists")
.upon_receiving("a request for a product")
.with_request("GET", "/api/v2/products/550e8400-e29b-41d4-a716-446655440000")
.will_respond_with(200, body=expected_body))
with pact:
result = ProductClient(pact.uri).get_product(
"550e8400-e29b-41d4-a716-446655440000"
)
assert result["name"] is not None
assert result["price"] >= 0
Верификация провайдера
Команда провайдера запускает верификацию, чтобы убедиться, что она соответствует всем ожиданиям потребителей:
# Provider verification (runs against the real provider)
from pact import Verifier
verifier = Verifier(
provider='ProductService',
provider_base_url='http://localhost:8080'
)
# Set up provider states
@verifier.provider_state('product ABC-123 exists')
def setup_product_exists():
db.insert_product(id='ABC-123', name='Blue Widget', price=29.99)
@verifier.provider_state('product NONEXISTENT does not exist')
def setup_product_not_exists():
db.delete_product(id='NONEXISTENT')
# Verify all pacts from the broker
success = verifier.verify_with_broker(
broker_url='https://pact-broker.example.com',
publish_verification_results=True,
provider_version='1.2.3'
)
assert success
Pact в CI/CD
Consumer CI:
1. Run consumer Pact tests → generates Pact file
2. Publish Pact to broker
3. Check "can I deploy?" against broker
Provider CI:
1. Pull consumer Pacts from broker
2. Run provider verification
3. Publish verification results
4. Check "can I deploy?" against broker
Брокер отслеживает, какие версии потребителей верифицированы с какими версиями провайдеров, обеспечивая безопасные независимые деплои.
Ключевой вывод
Подход Pact, управляемый потребителем, гарантирует, что изменения API не сломают потребителей -- проблема, которую традиционное API-тестирование упускает, потому что провайдеры тестируются изолированно. ИИ значительно ускоряет внедрение Pact, генерируя потребительские контракты из реального клиентского кода и устраняя главный барьер -- ручное написание контрактов.