This lesson writes the consumer side of a Pact contract from scratch. By the end you will have a working consumer test that generates a real Pact file — the artefact that travels to the provider team.
Setting the scene
Order Service (consumer) calls User Service (provider) at GET /users/{id} to validate a customer before creating an order. The consumer test defines exactly what Order Service needs: an id, name, and email — nothing else. The provider is free to return additional fields, but those three must be present and match the expected types. This is the core of consumer-driven contract testing: the consumer specifies its minimum requirements, not the provider's full API shape.
The consumer test in full
Here is the complete test class. Every annotation and method is explained immediately below.
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "UserService", port = "8090")
class UserServiceConsumerPactTest {
@Pact(consumer = "OrderService")
public RequestResponsePact userExists(PactDslWithProvider builder) {
return builder
.given("user 42 exists")
.uponReceiving("a GET request for user 42")
.path("/users/42")
.method("GET")
.willRespondWith()
.status(200)
.headers(Map.of("Content-Type", "application/json"))
.body(LambdaDsl.newJsonBody(body -> {
body.numberType("id", 42);
body.stringType("name", "Alice");
body.stringMatcher("email", ".+@.+\\..+", "alice@test.com");
}).build())
.toPact();
}
@Test
@PactTestFor(pactMethod = "userExists")
void shouldReturnUserWhenFound(MockServer mockServer) {
// Point your HTTP client at the Pact mock server
UserServiceClient client = new UserServiceClient(mockServer.getUrl());
User user = client.getUser(42L);
assertThat(user.getId()).isEqualTo(42L);
assertThat(user.getName()).isEqualTo("Alice");
assertThat(user.getEmail()).isNotBlank();
}
}What each piece does:
@PactConsumerTestExt— JUnit 5 extension that wires up the Pact mock server and handles Pact file generation after the test completes@PactTestFor(providerName, port)— names the provider and sets the port for the embedded mock server@Pact(consumer = "OrderService")— marks the method that builds a contract interaction; the consumer name appears in the generated filename.given("user 42 exists")— provider state: a string label that tells the provider test what precondition to set up before replaying this interaction.uponReceiving(...)— human-readable interaction description; appears in the Pact file and in test output to help identify failures.path("/users/42").method("GET")— the exact request the consumer will send.willRespondWith().status(200).body(...)— the response the consumer expects and that the mock server will return during the test
Matchers — the key to durable contracts
Matchers are what separate brittle contracts from useful ones. Without them, Pact encodes exact values into the JSON file. If the provider later returns user ID 99 instead of 42 — a completely valid response — provider verification fails because the exact value no longer matches. Matchers shift the assertion from value equality to structural compatibility.
The four matchers you will use most:
// 1. Type matcher — any number, not just 42
body.numberType("id", 42);
// 2. String type matcher — any string, not just "Alice"
body.stringType("name", "Alice");
// 3. Regex matcher — email-shaped string
body.stringMatcher("email", ".+@.+\\..+", "alice@test.com");
// 4. Array containing — at least one element of this shape
body.arrayContaining("tags", arr ->
arr.stringType("type", "customer")
.numberType("priority", 1)
);The example value you pass to numberType, stringType, and stringMatcher is used only inside the mock server during the consumer test. It does not constrain the provider's real response. The generated Pact file records the matcher rule — type, regex, or equality — not the literal value.
Handling the "user not found" case
A contract should cover the paths your consumer actually exercises. If Order Service handles a missing user by catching an exception and returning a 404, that error path needs its own interaction:
@Pact(consumer = "OrderService")
public RequestResponsePact userNotFound(PactDslWithProvider builder) {
return builder
.given("user 999 does not exist")
.uponReceiving("a GET request for non-existent user 999")
.path("/users/999")
.method("GET")
.willRespondWith()
.status(404)
.headers(Map.of("Content-Type", "application/json"))
.body("{\"error\": \"User not found\"}")
.toPact();
}
@Test
@PactTestFor(pactMethod = "userNotFound")
void shouldThrowWhenUserNotFound(MockServer mockServer) {
UserServiceClient client = new UserServiceClient(mockServer.getUrl());
assertThatThrownBy(() -> client.getUser(999L))
.isInstanceOf(UserNotFoundException.class);
}Each interaction is its own @Pact method tied to its own @Test via pactMethod. This keeps failures unambiguous — when the 404 interaction breaks in provider verification, the failure names exactly that interaction rather than a catch-all failure across a combined method.
Where the Pact file lands
After running the test suite, Maven writes the contract to target/pacts/OrderService-UserService.json. The file structure encodes each interaction alongside its matchers:
"matchingRules": {
"body": {
"$.id": { "matchers": [{ "match": "type" }] },
"$.name": { "matchers": [{ "match": "type" }] },
"$.email": { "matchers": [{ "match": "regex", "regex": ".+@.+\\..+" }] }
}
}Once the file exists, publish it to the Pact Broker so the provider team can download and verify against it:
# Publish via Maven plugin
mvn pact:publish \
-Dpact.broker.url=https://my-broker.pactflow.io \
-Dpact.broker.token=$PACT_BROKER_TOKEN \
-Dpact.consumer.version=$GIT_SHATagging the publication with $GIT_SHA is essential — it lets the Pact Broker track exactly which version of the consumer produced this contract, which is the information can-i-deploy queries later.
⚠️ Common mistakes
- Testing the mock, not the consumer's behaviour. The purpose of the
@Testmethod is to verify that YOUR HTTP client parses the response correctly and maps it to your domain object. If your test only assertsuser != null, it adds no value. Assert on individual fields —getId(),getName(),getEmail()— to confirm that your deserialisation logic actually works. - Writing one giant
@Pactmethod with every interaction. Each distinct consumer-provider interaction should be its own@Pactmethod paired with its own@Test. Grouping them makes failures ambiguous (which interaction caused it?) and means any single change forces full regeneration of a combined method. - Forgetting to point your HTTP client at
mockServer.getUrl(). If your client is hardcoded to a base URL set elsewhere in the Spring context, the mock server receives nothing and returns nothing — yet the test passes because your assertions run against null or stale data. Always construct your client usingmockServer.getUrl()as the base URL inside the test method.
🎯 Practice task
- Create a
UserServiceConsumerPactTestclass in a sample project. Write the@Pactmethod forGET /users/{id}using at minimum three fields with matchers (not exact values). Run the test and confirm the Pact file appears undertarget/pacts/. - Open the generated Pact JSON. Find the
matchingRulessection and verify that your matchers are present. If you usedstringType, confirm the matcher entry reads"match": "type"— not"match": "equality". - Add a second interaction for the 404 case — "given user 999 does not exist". Write the corresponding
@Testand confirm your HTTP client throws the right exception type rather than returning null. - Deliberately break the consumer test: change
mockServer.getUrl()to a hardcoded URL that does not exist. Run the test. Read the error you get and explain why it confirms the client is actually being exercised, rather than being bypassed. - Add the
pact-jvm-provider-mavenplugin to yourpom.xmland configure it with a placeholder broker URL. Runmvn pact:publishand read the error output — identify what would need to change (URL, token, version tag) for the publish to succeed.
The next lesson covers the provider side: how to download that Pact file from the broker, set up provider states, and run verification against a real running service.