Modern QA2026Контрактное тестирование с Pact, управляемое потребителем
Join

Course04 API & Contract Testing with AI

Cutting-edge · Chapter 04

Контрактное тестирование с 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 требует ручного написания контрактов -- утомительный процесс, который команды часто пропускают. ИИ меняет это:

  1. Анализ клиентского кода для автоматического определения паттернов вызовов API
  2. Генерация потребительских контрактов из реального использования, а не документации
  3. Обнаружение дрейфа контрактов при изменениях провайдера
  4. Предложение минимально совместимых контрактов при возникновении конфликтов

Анатомия контракта 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, генерируя потребительские контракты из реального клиентского кода и устраняя главный барьер -- ручное написание контрактов.