Документация Pact в SDK (контрактное тестирование)

Pact в SDK (контрактное тестирование)

SDK Mockarty (Go, Python, Java) предоставляют полный набор инструментов
для consumer-driven contract testing: клиент брокера, верификатор
провайдера и DSL для message-пактов. JSON на проводе — строгое
подмножество спецификации pact-foundation V3/V4,
поэтому pact-файлы, созданные SDK, проверяются любым совместимым
брокером (Pact Broker, PactFlow) и верификатором (pact-jvm,
pact-python, pact-go).

Кратко

Возможность Go Python Java
HTTP pact builder (V3/V4) ✅ ✅ ✅
Матчеры (Like / Regex / EachLike / …) ✅ ✅ ✅
Верификатор провайдера (HTTP) ✅ ✅ ✅
Message-pact DSL (Asynchronous/Messages) ✅ ✅ ✅
Верификатор провайдера (сообщения) ✅ ✅ ✅
Broker publish / fetch / can-i-deploy ✅ ✅ ✅
Публикация результата верификации ✅ ✅ ✅

Переменные окружения (совместимо с pact-foundation)

Клиенты брокера читают те же переменные, что и
pact-cli и официальные
библиотеки pact-jvm / pact-python:

  • PACT_BROKER_BASE_URL — URL брокера (обязательно)
  • PACT_BROKER_TOKEN — bearer-токен (приоритетнее)
  • PACT_BROKER_USERNAME / PACT_BROKER_PASSWORD — basic auth fallback

Bearer выигрывает у basic, если заданы оба.


1. Клиент брокера

Публикация записанного потребителем pact и запрос «можно ли
деплоить».

Go

import "github.com/mockarty/mockarty-go/pact"

bc, err := pact.NewBrokerClient()
if err != nil { return err }

err = bc.Publish(ctx, pactBytes, "1.2.3", "main", []string{"ci"})

res, err := bc.CanIDeploy(ctx, "OrderClient", "1.2.3", "production")
if !res.Deployable { log.Fatalf("ЗАБЛОКИРОВАНО: %s", res.Reason) }

Python

from mockarty.pact import BrokerClient

bc = BrokerClient()
bc.publish(pact_bytes, consumer_version="1.2.3", branch="main", tags=["ci"])

res = bc.can_i_deploy("OrderClient", "1.2.3", to_environment="production")
if not res.deployable:
    raise SystemExit(f"ЗАБЛОКИРОВАНО: {res.reason}")

Java

import ru.mockarty.pact.broker.BrokerClient;
import ru.mockarty.pact.broker.CanIDeployResult;

BrokerClient bc = BrokerClient.fromEnv();
bc.publish(pactBytes, "1.2.3", "main", List.of("ci"));

CanIDeployResult res = bc.canIDeploy("OrderClient", "1.2.3", "production");
if (!res.deployable()) throw new IllegalStateException(res.reason());

2. Верификатор провайдера (HTTP)

Запускается на стороне провайдера: достаёт опубликованный
потребителем pact, проигрывает каждое взаимодействие против работающего
провайдера, сверяет ответ с записанной формой и публикует результат
обратно в брокер.

Go

v, _ := pact.NewVerifier(
    pact.WithProviderURL("http://localhost:8080"),
    pact.WithProviderName("OrderAPI"),
    pact.WithProviderVersion(os.Getenv("GIT_COMMIT")),
    pact.WithBrokerClient(bc),
    pact.WithStateHandler("order 42 exists",
        func(ctx context.Context, s string, p map[string]any) error {
            return seedOrder(42)
        }),
)
res, _ := v.VerifyFromBroker(ctx, "OrderClient", "OrderAPI", "latest")
if !res.OK() { return fmt.Errorf("verification failed") }
_ = v.PublishResults(ctx, "OrderClient", "OrderAPI", "1.0", res)

Python

from mockarty.pact import Verifier

v = (Verifier(provider_url="http://localhost:8080",
              provider_name="OrderAPI",
              provider_version=os.environ["GIT_COMMIT"])
     .with_broker(bc)
     .with_state_handler("order 42 exists",
                         lambda state, params: seed_order(42)))

result = v.verify_from_broker("OrderClient", "OrderAPI", "latest")
assert result.ok, [ir.error or ir.mismatches for ir in result.interactions if not ir.passed]
v.publish_results("OrderClient", "OrderAPI", "1.0", result)

Java

Verifier v = Verifier.builder()
    .providerUrl("http://localhost:8080")
    .providerName("OrderAPI")
    .providerVersion(System.getenv("GIT_COMMIT"))
    .broker(bc)
    .stateHandler("order 42 exists", (state, params) -> seedOrder(42))
    .build();

VerificationResult res = v.verifyFromBroker("OrderClient", "OrderAPI", "latest");
if (!res.ok()) throw new AssertionError(res.summary());
v.publishResults("OrderClient", "OrderAPI", "1.0", res);

Provider-state хуки

Верификатор разрешает providerStates двумя способами:

  1. Inline-хендлер — WithStateHandler / .with_state_handler /
    .stateHandler для in-process настройки.
  2. State setup URL — HTTP-эндпоинт провайдера
    (POST /_pact/provider_states по соглашению pact-foundation).
    Верификатор шлёт {"state","params","action":"setup"}.

Поддерживаются и V3 (одиночный providerState), и V4 (массив
providerStates).

Перезапись запроса

Добавьте auth-заголовки, идентификаторы тенанта, JWT и т.д. перед
повтором запроса:

  • Go: pact.WithRequestFilter(func(ctx, req) error { ... })
  • Python: .with_request_filter(lambda req: req["headers"]...
  • Java: .requestFilter(req -> req.headers().put(...))

3. Message-pact DSL (Asynchronous/Messages)

Контракты на асинхронные сообщения — события Kafka, сообщения AMQP,
SNS-уведомления, NATS-сабжекты. Потребитель декларирует ожидаемую
форму сообщения, верификатор провайдера проигрывает per-description
producer’ы и сопоставляет их байты с записанной формой.

Сторона потребителя

Go

mp := pact.NewMessagePact("OrderConsumer", "OrderEvents")
mp.Given("user 42 exists").
    ExpectsToReceive("an order-created event").
    WithMetadata(map[string]string{"topic": "orders"}).
    WithContent(map[string]any{
        "orderId": pact.Like(42),
        "status":  pact.Regex("open", "^(open|closed)$"),
    })

if err := mp.Verify(handleOrderEvent); err != nil { t.Fatal(err) }
_, _ = mp.WriteFile("./pacts")

Python

from mockarty.pact import MessagePact, Like, Regex

mp = (MessagePact("OrderConsumer", "OrderEvents")
      .given("user 42 exists")
      .expects_to_receive("an order-created event")
      .with_metadata({"topic": "orders"})
      .with_content({
          "orderId": Like(42),
          "status":  Regex(r"^(open|closed)$", "open"),
      }))

mp.verify(handle_order_event)
mp.write_file("./pacts")

Java

import ru.mockarty.pact.message.MessagePact;

MessagePact mp = new MessagePact("OrderConsumer", "OrderEvents")
    .given("user 42 exists")
    .expectsToReceive("an order-created event")
    .withMetadata(Map.of("topic", "orders"))
    .withContent(Map.of("orderId", 42, "status", "open"));

mp.verify((bytes, meta) -> orderHandler.handle(bytes));
mp.writeFile(Path.of("./pacts"));

Сторона провайдера

Зарегистрируйте producer по description, возвращающий байты
и метаданные сообщения, которое опубликовал бы реальный провайдер:

Go

v, _ := pact.NewVerifier(
    pact.WithProviderURL("http://x"),
    pact.WithMessageProducer("an order-created event",
        func(ctx context.Context, desc string, states []pact.ProviderState) ([]byte, map[string]string, error) {
            return produceOrderCreated(states[0].Params["orderId"])
        }),
)
res, _ := v.VerifyMessagePactBytes(ctx, pactBytes)
if !res.OK() { return fmt.Errorf("message verification failed") }

Python

v = Verifier(provider_url="http://x").with_message_producer(
    "an order-created event",
    lambda desc, states: (produce_order_created(states[0]["params"]["orderId"]), {}),
)
res = v.verify_message_pact_bytes(pact_bytes)
assert res.ok, [ir.error or ir.mismatches for ir in res.interactions if not ir.passed]

Java

Verifier v = Verifier.builder()
    .providerUrl("http://x")
    .messageProducer("an order-created event",
        (desc, states) -> {
            @SuppressWarnings("unchecked")
            Map<String, Object> params = (Map<String, Object>) states.get(0).get("params");
            return new Verifier.MessagePayload(
                produceOrderCreated((Integer) params.get("orderId")), Map.of());
        })
    .build();
VerificationResult res = v.verifyMessagePactBytes(pactBytes);

4. Совместимость со спецификацией

  • V4 (по умолчанию) — interactions c type: "Asynchronous/Messages"
    для message-пактов и type: "Synchronous/HTTP" для HTTP.
  • V3 (legacy) — одиночный providerState и массив messages
    верхнего уровня. Выбирается через .WithSpecVersion(SpecV3) (Go),
    параметр spec="3.0.0" (Python), new MessagePact("c","p","3.0.0") (Java).

Верификаторы во всех трёх SDK читают обе формы — на стороне
провайдера не нужно совпадать со spec-версией потребителя.


5. Коды возврата и интеграция CI

  • BrokerException / BrokerError пробрасывает статус и тело ответа
    брокера, чтобы CI-скрипт логировал upstream-ошибку, а не generic
    «request failed».
  • CanIDeploy(...) возвращает deployable: false с непустым reason
    — на этом сигнале ронять пайплайн, а не на transport-ошибках.
  • Результаты верификации публикуются в /verification-results
    брокера — матрица совместимости обновляется автоматически.

При расхождении смотрите на per-interaction-результат:
InteractionResult содержит error (ошибка state-setup / transport
/ filter) или mismatches (поле за полем, как у инструментов
pact-foundation).