Tax / Fiscal · Contract

Contract Testing Strategies

Contract Testing Strategies is the work that makes the systems talk. The API is the contract between the producer and the consumer; the contract is what determines whether the.

John Kihiu12 min read

Contract testing only pays off if it's run at the right point in the pipeline and covers the right kind of change. A consumer-driven contract suite that nobody wires into a deployment gate is just a set of tests that pass in isolation and get ignored — the strategy question is less "which library" and more "where does this actually block a bad deploy."

Pick the boundary that actually breaks often

Don't try to contract-test every service pair at once. Look at your incident history for the integrations that have actually caused production breakage from a schema or behaviour change — usually a small number of high-traffic boundaries account for most of the pain. Start there. Contract testing has genuine setup cost (a broker, CI wiring on both provider and consumer sides, buy-in from both teams to run verification on every change), and spreading that cost thin across every service pair before proving the workflow on one is how these initiatives stall.

Version the contract, not just the API

A contract needs its own lifecycle independent of the API's version number. When a consumer adds a new expectation, that's a new contract version, verified independently; when a provider makes a genuinely breaking change, the old contract version should fail verification loudly rather than getting silently replaced. Most brokers (Pact Broker, PactFlow) track contract versions against the git commit or CI build that generated them, which is what lets the can-i-deploy check answer "has the *currently deployed* provider verified *this exact* consumer contract" rather than just "has this contract ever passed."

BASH · DEPLOYMENT GATE
pact-broker can-i-deploy \
  --pacticipant InvoiceService \
  --version "$GIT_SHA" \
  --to-environment production \
  --broker-base-url https://pact-broker.internal \
  --broker-token "$PACT_BROKER_TOKEN"

# Non-zero exit blocks the deploy step in CI

Provider states keep verification honest

A contract test that says "GET /customers/42 returns a customer" is meaningless unless the provider's verification step actually has a customer 42 to return. Provider states — the given('customer 42 exists') setup in a Pact test — tell the provider's verification harness what fixture data to seed before replaying that interaction. Skipping this and verifying against whatever happens to be in a shared test database is how contract verification passes locally and fails in CI, or worse, passes in CI against stale fixtures and misses a real regression.

Don't let contract tests become a second copy of your business-logic tests

Contract tests exist to verify the shape and behaviour of a boundary, not to re-verify every business rule the provider enforces. If you find yourself writing a contract test for every edge case in a discount calculation, that logic belongs in the provider's own unit tests — the contract only needs to prove the endpoint returns the right shape, and consumers only need one or two representative interactions per endpoint they depend on.

Bi-directional contract testing for teams that can't share a broker

Standard Pact requires the consumer and provider to publish to a shared broker, which assumes a level of cross-team coordination that doesn't always exist — particularly with external or third-party APIs. Bi-directional contract testing (PactFlow's newer approach, and the general pattern behind tools like Spring Cloud Contract in some configurations) generates the contract from the provider's own OpenAPI spec or existing tests and compares it independently against the consumer's expectations, without requiring both sides to run the same tooling. It's a reasonable fallback when full Pact adoption isn't realistic, though it catches a narrower set of mismatches than true consumer-driven verification.

Treat a broken contract verification as a merge blocker, not a warning

A contract test that's allowed to fail without blocking the provider's merge is a contract test that will be ignored the first time it's inconvenient. If the team isn't willing to treat verification failures as blocking, the honest conclusion is that contract testing isn't actually load-bearing yet — fix that before investing further in coverage.

StrategyBest forCoordination needed
Consumer-driven (Pact + broker)Internal services, both teams cooperativeHigh — shared broker, CI on both sides
Bi-directional (spec-derived)Third-party or slow-moving external APIsLow — no shared broker required
Schema diffing (OpenAPI/JSON Schema)Catching structural drift onlyLow, but misses behavioural mismatches
Full end-to-end integration testsA handful of critical user journeysMedium — needs a real shared environment

Wrapping up

A contract testing strategy succeeds or fails on where you point it and whether verification actually blocks a bad deploy — not on which library generates the contract files. Pick the one or two boundaries that have caused real incidents, wire can-i-deploy into the release gate on both sides, and only then think about expanding coverage to the rest of the service graph.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.