Docs SDK Pact (Contract Testing)

SDK Pact (Contract Testing)

Mockarty SDKs ship a complete consumer-driven contract-testing
toolkit — broker client, provider verifier, and message-pact DSL —
across all three official SDKs (Go, Python, Java). The on-the-wire
JSON is a strict subset of the
pact-foundation V3/V4 spec,
so pact files produced by these SDKs verify against any compatible
broker (Pact Broker, PactFlow) and provider verifier (pact-jvm,
pact-python, pact-go).

At a glance

Capability Go Python Java
HTTP pact builder (V3/V4) ✅ ✅ ✅
Matchers (Like / Regex / EachLike / …) ✅ ✅ ✅
Provider verifier (HTTP) ✅ ✅ ✅
Message-pact DSL (Asynchronous/Messages) ✅ ✅ ✅
Provider verifier (messages) ✅ ✅ ✅
Broker publish / fetch / can-i-deploy ✅ ✅ ✅
Verification result publish ✅ ✅ ✅

Environment variables (pact-foundation compatible)

The broker clients read the same variables as
pact-cli and the
official pact-jvm/pact-python libraries, so existing CI scripts work
without changes:

  • PACT_BROKER_BASE_URL — broker URL (required)
  • PACT_BROKER_TOKEN — bearer token (preferred when available)
  • PACT_BROKER_USERNAME / PACT_BROKER_PASSWORD — basic-auth fallback

Bearer wins over basic when both are set.


1. Broker client

Publish a recorded consumer pact and ask the broker if it is safe
to deploy.

Go

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

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

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

res, err := bc.CanIDeploy(ctx, "OrderClient", "1.2.3", "production")
if !res.Deployable { log.Fatalf("BLOCKED: %s", res.Reason) }

Python

from mockarty.pact import BrokerClient

bc = BrokerClient()                          # reads env
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"BLOCKED: {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. Provider verifier (HTTP)

Run on the provider side: pulls the consumer-published pact,
replays every interaction against the running provider, matches the
actual response against the recorded shape, and (optionally) publishes
the verification outcome back to the broker.

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

import ru.mockarty.pact.verifier.Verifier;
import ru.mockarty.pact.verifier.VerificationResult;

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 hooks

The verifier resolves each interaction’s providerStates in two ways:

  1. In-process handler — WithStateHandler(name, fn) (Go),
    .with_state_handler(name, fn) (Python),
    .stateHandler(name, fn) (Java).
  2. State-setup URL — point at an HTTP endpoint your provider
    exposes (POST /_pact/provider_states is the pact-foundation
    convention). The verifier POSTs {"state","params","action":"setup"}.

Both V3 (singular providerState) and V4 (plural providerStates)
shapes are accepted.

Request rewriting

Add auth headers, tenant identifiers, signed JWTs, etc. before the
verifier replays the consumer’s request:

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

3. Message-pact DSL (Asynchronous/Messages)

Async messaging contracts — Kafka events, AMQP messages, SNS
notifications, NATS subjects. The consumer declares the message shape
it expects to receive; the provider verifier replays per-description
producers and matches their bytes against the recorded shape.

Consumer side

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)$"),
    })

// 1) Verify the consumer's real handler can decode the example bytes.
if err := mp.Verify(handleOrderEvent); err != nil { t.Fatal(err) }
// 2) Write the pact file (then publish via BrokerClient.Publish).
_, _ = 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"));

Provider side

Register a per-description producer that returns the bytes (and
metadata) your provider would publish for that interaction:

Go

v, _ := pact.NewVerifier(
    pact.WithProviderURL("http://x"),  // any non-empty value — messages skip HTTP
    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)

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);

The verifier matches each producer’s bytes against the recorded
contents.content using the same matcher engine used for HTTP
response matching.


4. Spec compatibility

  • V4 (default) — emits interactions with type: "Asynchronous/Messages"
    for message pacts, type: "Synchronous/HTTP" for HTTP pacts.
  • V3 (legacy) — emits the singular providerState and the
    top-level messages array. Select via .WithSpecVersion(SpecV3)
    (Go), spec="3.0.0" constructor arg (Python), or
    new MessagePact("c","p","3.0.0") (Java).

Verifiers in all three SDKs read both shapes — you don’t need to
match the consumer’s spec version on the provider side.


5. Exit codes and CI integration

  • Broker BrokerException / BrokerError surfaces the HTTP status
    and response body, so CI scripts can log the upstream broker error
    rather than a generic “request failed”.
  • CanIDeploy(...) returns deployable: false with a non-empty
    reason — fail the pipeline on that signal, not on transport errors.
  • Verification results are JSON-serialised into the broker’s
    /verification-results endpoint so the broker’s compatibility
    matrix updates automatically.

When something doesn’t add up, run with the verifier’s per-interaction
output: each InteractionResult carries error (state setup /
transport / filter failure) or mismatches (recorded vs. actual
field-by-field), exactly as the pact-foundation tooling does.