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:
- In-process handler —
WithStateHandler(name, fn)(Go),
.with_state_handler(name, fn)(Python),
.stateHandler(name, fn)(Java). - State-setup URL — point at an HTTP endpoint your provider
exposes (POST /_pact/provider_statesis the pact-foundation
convention). The verifierPOSTs{"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
interactionswithtype: "Asynchronous/Messages"
for message pacts,type: "Synchronous/HTTP"for HTTP pacts. - V3 (legacy) — emits the singular
providerStateand the
top-levelmessagesarray. 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/BrokerErrorsurfaces the HTTP status
and response body, so CI scripts can log the upstream broker error
rather than a generic “request failed”. CanIDeploy(...)returnsdeployable: falsewith a non-empty
reason— fail the pipeline on that signal, not on transport errors.- Verification results are JSON-serialised into the broker’s
/verification-resultsendpoint 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.