skip to content

How does a Pact provider verification run fetch pacts and replay each interaction?

level: middleimportance: must knowfreq 72%

answer

  1. the run belongs to one side only
  2. pacts have to come from somewhere
  3. the provider answers on a real port
  4. one test case per interaction
  5. state setup, replay, then compare

basics

~20 s

A Pact verification run loads pact files from a broker, a URL or a local folder, then per interaction applies the provider state, sends the recorded request to the running provider, and matches the real response against the recorded expectations.

solid answer

~50 s

Verification is driven from the provider's own test run, not the consumer's. A pact source is configured first - `@PactBroker` to fetch from a broker, `@PactUrl` for a fixed URL, or `@PactFolder` for pacts on disk - and the provider application is started for real, with the verification context's target pointed at its listening port. Pact-JVM then expands every interaction in every matching pact into its own JUnit test case via `@TestTemplate` and `PactVerificationInvocationContextProvider`. For each one it applies the interaction's provider state, replays the recorded request verbatim - method, path, query, headers, body - over real HTTP, and compares the actual response against the recorded one using the matching rules stored in the pact. The consumer's code never runs; only its recorded expectations do. A mismatch fails that interaction's test and names the exact path that differed.

code

java · 19 lines
java
@Provider("grant-portal-api")
@PactBroker(url = "https://broker.internal")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GrantPortalPactVerificationTest {

    @LocalServerPort
    int port;

    @BeforeEach
    void setTarget(PactVerificationContext context) {
        context.setTarget(new HttpTestTarget("localhost", port));
    }

    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider.class)
    void verifyInteraction(PactVerificationContext context) {
        context.verifyInteraction();
    }
}

go deeper

for a junior

Be able to say that contract verification runs in the provider's build and replays requests a consumer recorded earlier. Configuring a run is not expected of you, but do not describe it as running the consumer's tests again.

for a middle

Expect to walk the run end to end: where pacts are loaded from, how the provider is started and targeted, and the setup-replay-compare sequence for each interaction. Know that each interaction becomes its own reported test case.

for a senior

Be ready to explain why an added response field does not break a pact but a changed array size does, and to triage a real mismatch report without hand-editing the pact or loosening the provider until the message disappears.

for a principal

Own the wiring decisions: which pact source CI uses, why a run reading pact files off disk can never report anywhere, and how the suite stays trustworthy as the number of consumers and interactions grows.

## Verification runs in the provider's build A pact is written by a consumer's test and read by a provider's test. Verification is therefore a job in the **provider's** pipeline: the consumer's repository is not checked out, its HTTP client never executes, and its serialisers, retries and timeouts are not exercised. What arrives is a JSON document containing interactions - each one a recorded request, an expected response, the matching rules that say how loosely each part may be compared, and zero or more named provider states. ## Choosing a pact source The first decision the test makes is where pacts come from, and it has consequences beyond convenience. | Source | Pact-JVM annotation | What it is for | | --- | --- | --- | | Pact Broker | `@PactBroker` | CI runs: fetch every pact for this provider and report results back | | A fixed URL | `@PactUrl` | Reproducing one specific failing pact by hand | | A local directory | `@PactFolder` | Bootstrapping before a broker exists | Only a pact fetched from a broker carries the link the verifier needs to report results afterwards, so a folder-based run is a local debugging tool and nothing more. In pact-js the same choice is made through verifier options rather than annotations - a broker URL, or an explicit list of pact file locations. ## Starting the provider and aiming the verifier The verifier is an ordinary HTTP client, so something has to make the provider answer on a port first. The usual shape is the provider's own Spring Boot test bootstrap on a random port; the test then tells the run where to send traffic by setting the target on the verification context to that host and port. Nothing about the provider is replaced or stubbed - the point of the run is to exercise the real routing, deserialisation, business code and serialisation. Expansion happens next. Pact-JVM's JUnit 5 integration uses a `@TestTemplate` method plus `PactVerificationInvocationContextProvider` to turn the fetched pacts into **one JUnit test case per interaction**, named after the interaction description and its state. A provider with four consumers and 23 interactions between them reports 23 independently red-or-green tests, which is why a failure names itself instead of collapsing into "the pact failed". ## What replaying one interaction does For each interaction the run performs the same five steps: 1. **Apply the provider states.** Each state named on the interaction is set up - by invoking the matching handler in the test class, or by calling a configured state-change endpoint. This is where the data the request will read comes into existence. 2. **Send the recorded request.** Method, path, query string, headers and body go out exactly as recorded. Pact Specification V3 generators may substitute a value at this moment - a fresh timestamp, or an identifier taken from a state parameter - so the bytes are not always byte-identical to the recording, but nothing else varies. 3. **Capture the real response.** Status, headers and body as the provider actually produced them. 4. **Compare.** The actual response is matched against the expected one using the matching rules stored in the pact, not by string equality. 5. **Tear down**, if the state declares a teardown step. Interactions are independent by design. Nothing guarantees the order they run in, and a suite that only passes in one order is telling you the provider states are leaking into each other. ## The comparison is deliberately asymmetric This is where most of the surprise lives, and it is intentional - the pact records what one consumer needs, not the shape of the whole API. - **Status** must equal the recorded status exactly. - **Headers the pact names** must match; headers the provider adds that the pact never mentioned are ignored. - **Body keys the pact records** must be present and satisfy their matchers; **keys the pact does not mention are ignored**. A provider that grows its response from twelve fields to seventeen does not fail a pact that only ever asked for five of them. - **Arrays are stricter than objects**: element counts matter unless the consumer relaxed them with a minimum-size type matcher. - A recorded value such as `68341` may be asserted as "an integer" rather than as that literal, if the consumer recorded a type matcher instead of an exact one. ## Reading a failure A mismatch reports the JSON path that differed and what was expected there - for instance, that `$.applicant.postcode` was expected to be a string matching a pattern and came back `null`. Two reflexes are wrong. Editing the pact file by hand is pointless: it is generated by the consumer's test and, in CI, re-fetched from the broker on every run. Loosening the provider's response until the message stops is worse, because the message is the contract talking. The real first question is which side moved - a provider regression, or a consumer expectation that nobody has agreed to yet. When one interaction needs isolating during debugging, Pact-JVM exposes filter system properties that restrict the run by consumer name, interaction description or provider state, so a 23-case suite can be narrowed down to the single case that is red. Those filters are a debugging aid only: a filtered run must never be the run that reports results, because it would record an outcome for interactions it never replayed.

  • The provider returns extra fields the pact never mentions. Why does verification still pass?
    Pact asserts only what the consumer recorded it needs, so keys in the actual response body that the pact does not mention are ignored. That asymmetry is deliberate: it lets a provider add fields without breaking every consumer. Arrays are the exception - element counts matter unless the consumer recorded a minimum-size type matcher rather than a fixed list.
  • How does the request the verifier sends differ from the one the consumer's own test sent?
    The verifier replays what the pact recorded, not the consumer's client code. From Pact Specification V3, generators can substitute a dynamic value at replay time - a fresh timestamp, or an identifier taken from a provider-state parameter - so the bytes need not be identical. Nothing in the consumer's HTTP client, retry policy or serialisation is exercised by the provider run.
  • How do you narrow a verification run to one failing interaction while debugging?
    Pact-JVM exposes filter system properties that restrict a run by consumer name, interaction description or provider state, so a large suite collapses to the single red case. Treat it strictly as a local aid: a filtered run must never publish results, because it would record an outcome covering interactions it never replayed.

The pact file is a tape and the verification run is the playback head: the consumer recorded the conversation once, and the provider is asked to produce the same replies at every mark on the tape.

saying these in an interview costs you the question

  • Says the consumer's test code is re-executed during provider verification
  • Thinks the provider is mocked or stubbed while its pacts are verified
  • Expects the whole pact file to pass or fail as one test
  • Believes the response must match field for field, extras included
  • Cannot name any pact source other than a file in the provider repo