The consumer test generates a contract and publishes it. This lesson covers the other half: how the provider downloads that contract, verifies its API satisfies every interaction, and how the Pact Broker ties the two halves together into a deployment safety net.
Provider verification test
The provider test is structurally different from a unit or component test — you do not write individual @Test methods for each interaction. Instead, Pact generates one test invocation per interaction at runtime by reading the Pact file. Your job is to start the real service, implement @State setup methods, and let Pact replay the interactions.
@Provider("UserService")
@PactBroker(
url = "${PACT_BROKER_URL}",
authentication = @PactBrokerAuth(token = "${PACT_BROKER_TOKEN}")
)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class UserServiceProviderPactTest {
@LocalServerPort
private int port;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", port));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void verifyPact(PactVerificationContext context) {
context.verifyInteraction();
}
@State("user 42 exists")
void setupUser42() {
userRepository.deleteAll();
userRepository.save(new User(42L, "Alice", "alice@test.com"));
}
@State("user 999 does not exist")
void ensureUser999Missing() {
userRepository.deleteById(999L);
}
}What each piece does:
@Provider("UserService")— identifies this as the provider side of the UserService contract; must match theproviderNamein the consumer's@PactTestForannotation exactly@PactBroker(...)— tells Pact where to download consumer contracts from; reads the broker URL and authentication token from environment variables so credentials are never committed to source control@SpringBootTest(RANDOM_PORT)— starts the real service on a random port; Pact replays each request against this live instance rather than a mock@TestTemplate+PactVerificationInvocationContextProvider— Pact dynamically generates one test invocation per interaction in each downloaded Pact file; you never write these invocations manually@Statemethods — set up the database state that matches the consumer's.given(...)clause before each interaction is replayed; the string value must be an exact match
What provider verification checks
When the test runs, Pact replays each request from the Pact file against the real running service and compares the response against the contract. The comparison uses the matchers recorded in the Pact file — not exact value equality.
The rule is: the provider may return additional fields, but must include every field the consumer defined at the specified type.
Consider a consumer contract that says { id: number, name: string }:
- Provider returns
{ id: 1, name: "Alice", role: "admin", createdAt: "2024-01-01" }→ PASS — extra fields are allowed - Provider returns
{ id: 1, fullName: "Alice" }→ FAIL — the requirednamefield is absent, andfullNamewas never declared in the contract
This asymmetry matters in practice. Provider teams can freely extend their responses with new fields. They cannot remove or rename fields that any consumer has declared in a published contract without first migrating those consumers.
Publishing verification results
After provider verification runs, results are published back to the Pact Broker automatically (configured via @PactBroker). The broker records: "UserService at version $SHA has verified OrderService's Pact file at version $SHA — result: passed." This record is what the can-i-deploy query reads.
The can-i-deploy command is added as a CI gate immediately before any deployment step:
# In UserService CI, before deploying to production:
pact-broker can-i-deploy \
--pacticipant UserService \
--version $GIT_SHA \
--to-environment productionThe output is binary: Computer says yes ✔ when the version being deployed is compatible with every consumer version already deployed to that environment, or Computer says no ✗ with a specific reason — for example, "OrderService version abc123 has not been verified against UserService version def456."
The Pact Broker
The broker is the shared registry that makes consumer-driven contract testing work across separate repositories and deployment pipelines.
- Contract storage — holds every Pact file published by every consumer, versioned by Git SHA; old versions are retained so you can query historical compatibility
- Verification results — records which provider version successfully verified which consumer contract version; without this record,
can-i-deployhas no data to query - Dependency matrix — a table showing which version of each consumer is compatible with which version of each provider; the broker renders this as a visual grid in the UI
can-i-deploy— the key safety query: "can I deploy this version to this environment given the versions already deployed there?" It reads the dependency matrix and returns a pass/fail- Webhooks — trigger provider verification pipelines automatically when a consumer publishes a new Pact, so the provider team does not need to poll or coordinate manually
For hosting, you have two options. The self-hosted open-source broker is a Docker image you run yourself — free, covers the core features above. PactFlow is the managed hosted version, adding bi-directional contract testing (for teams that cannot modify the provider test), advanced analytics, a richer UI, and SLA-backed availability. For most teams starting out, the self-hosted broker is sufficient.
Handling breaking changes
The real test of contract testing is what happens when someone tries to make a breaking change. Walk through a concrete scenario: Order Service renames the field it reads from name to fullName. The developer updates the consumer test, runs it, generates a new Pact file, and publishes it to the broker. The new Pact file now requires fullName instead of name.
The provider's CI runs its verification test. User Service still returns name. The interaction for fullName fails. The broker records a failed verification. When the Order Service developer runs can-i-deploy for their new consumer version, the command returns no — because the provider version currently deployed to production has not verified the new Pact. The deployment is blocked.
Two resolution paths:
- Provider adds
fullNamealongsidename(backward compatible). Provider deploys. Now both fields exist. Consumer can deploy. Provider later removesnameonce all consumers have migrated. This is the lower-risk path and the one the Pact workflow is designed to support. - Both teams coordinate a simultaneous deploy window. This requires locking deployment pipelines across two services, increases blast radius, and is harder to roll back. Avoid it when option 1 is possible.
For how this fits into a full deployment pipeline with environment promotion gates, cross-reference the CI/CD for QA course.
- – Pact file + Git SHA
- – Triggered by CI
- – All interactions captured
- – Downloads Pact files
- – Runs @State setup
- – Publishes results
- – Checks compatibility matrix
- – CI gate before deploy
- – Env-aware versioning
- All consumer versions –
- All provider versions –
- Compatibility record –
⚠️ Common mistakes
- Not implementing
@Statemethods for every provider state the consumer defines. If Order Service's Pact says "given user 42 exists" but the provider test has no@State("user 42 exists")method, the verification will fail with a 404 because the user does not exist in the test database when Pact replays the request. Every.given(...)string in the consumer test needs a matching@Statemethod on the provider side — the strings must match exactly, including casing. - Running provider verification against a stub or mock instead of the real running service. The entire value of provider verification is that it runs against real code. Running against a mock proves only that a mock works. Use
@SpringBootTest(RANDOM_PORT)so the full application context — including your controllers, service layer, and database queries — executes during verification. - Ignoring
can-i-deployresults in CI. Teams often add Pact testing without adding thecan-i-deploygate. Without the gate, contracts are verified but deployments are not blocked when verifications fail. The feedback loop exists but is not enforced. Add the command as a required CI step — treat anoanswer the same way you would treat a failing test suite.
🎯 Practice task
- Add the Pact provider JUnit 5 dependency to your User Service project. Write the
@Providertest class shown above. Add a@State("user 42 exists")method that inserts a test user via the repository. Confirm the class compiles. - Run the provider test locally — you will get a connection error because there is no broker yet. Replace
@PactBrokerwith@PactFolder("../order-service/target/pacts")to load the Pact file directly from the consumer's output directory. Re-run and confirm verification attempts to execute. - Make the verification pass end-to-end. Then deliberately break it: rename the
namefield in User Service's JSON response tofullName. Run the provider test again. Read the failure message and identify exactly what mismatch it reports and which interaction failed. - Restore the
namefield and add a new@State("user 999 does not exist")method. Add the corresponding 404 interaction to the consumer Pact and re-run both the consumer and provider tests to confirm both pass. - Read the
can-i-deploydocumentation at docs.pact.io. Write the exactpact-broker can-i-deploycommand you would add as a GitHub Actions workflow step to gate deployment of UserService to production, using${{ github.sha }}as the version.
With consumer tests generating contracts and provider verification enforcing them, you now have a complete contract-testing loop. The next lesson extends this foundation to asynchronous messaging contracts — where services communicate via events rather than HTTP.