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 двумя способами:
- Inline-хендлер —
WithStateHandler/.with_state_handler/
.stateHandlerдля in-process настройки. - 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 (по умолчанию) —
interactionsctype: "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).