Consumer-Driven Contract Testing
Consumer-Driven Contract Testing
Consumer-driven contract testing is an approach to testing API interactions between services where the consumer of an API defines the expectations (the "contract"), and those expectations are verified against the provider as part of the provider's CI pipeline.
This inverts the traditional "provider publishes OpenAPI spec, consumer adapts" model — the consumer specifies exactly what it needs, and the provider's test suite enforces that the provider continues to satisfy those needs.
The Problem It Solves
In a microservice architecture, services communicate over HTTP (or messaging). Two types of tests exist for this:
- Integration tests with real downstream services — slow, fragile, require full environment
- Mocked integration tests with WireMock stubs — fast, but stubs can drift from the real API
Consumer-driven contracts solve the drift problem: the stubs used by the consumer are generated from contracts that are also verified against the real provider. If the provider changes its API in a way that breaks the contract, the provider's own CI pipeline fails.
Key Roles
| Role | Description | Writes | Runs |
|---|---|---|---|
| Consumer | Service that calls the API (e.g., BFF calling User Service) | Contracts | Consumer integration tests using generated stubs |
| Provider | Service that owns the API (e.g., User Service) | — | Provider verification tests (generated from consumer contracts) |
Workflow
1. Consumer team writes contracts
(src/test/resources/contracts/user-service/*.groovy)
│
▼
2. Build tool generates:
├── WireMock stub JARs (for consumer integration tests)
└── Provider verification test classes (for provider CI)
│
├──▶ Consumer CI: runs integration tests
│ using WireMock stubs → fast, no network
│
└──▶ Provider CI: runs provider verification tests
against the running provider service
→ if provider breaks contract, CI fails
Spring Cloud Contract
Spring Cloud Contract is the Spring ecosystem implementation. It supports:
- Groovy DSL and YAML contract definitions
- Generation of WireMock stubs (for consumer tests)
- Generation of JUnit 5 or Spock provider verification tests
- HTTP and messaging (Kafka, RabbitMQ) contracts
- Stub Runner for consumer-side automatic stub loading
Consumer Dependencies
// Consumer (BFF) build.gradle
testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'Provider Dependencies
// Provider (User Service) build.gradle
testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
// Plugin generates provider verification tests
plugins {
id 'org.springframework.cloud.contract' version '4.1.x'
}
contracts {
// Where to find the consumer contracts
contractsDslDir = file("src/test/resources/contracts")
// Package for generated test classes
basePackageForTests = "com.example.userservice.contract"
// Base class that sets up the running application
baseClassForTests = "com.example.userservice.contract.ContractVerifierBase"
}Contract Definition (Groovy DSL)
Minimal HTTP GET Contract
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Consumer gets user by ID"
request {
method GET()
url "/users/42"
headers {
header("Authorization", matching("Bearer .+"))
header("Accept", "application/json")
}
}
response {
status OK() // 200
headers {
contentType applicationJson()
}
body([
id : 42,
name : "Jane Doe",
email: "jane@example.com"
])
}
}Contract with Body Matchers (Flexible Verification)
Contract.make {
description "Consumer gets any user — flexible response"
request {
method GET()
url $(consumer(regex('/users/[0-9]+')), producer('/users/42'))
headers {
header("Authorization", matching("Bearer .+"))
}
}
response {
status OK()
headers {
contentType applicationJson()
}
body([
id : $(producer(42), consumer(anyPositiveInt())),
name : $(producer("Jane Doe"), consumer(anyNonEmptyString())),
email: $(producer("jane@example.com"), consumer(anyEmail()))
])
bodyMatchers {
jsonPath('$.id', byRegex('[0-9]+'))
jsonPath('$.name', byRegex('[A-Za-z ]+'))
jsonPath('$.email', byRegex('.+@.+\\..+'))
}
}
}The $(consumer(...), producer(...)) syntax allows different values for stub generation (consumer side) vs. provider verification (provider side).
Messaging Contract (Kafka)
Contract.make {
description "Order placed event"
label "order_placed"
input {
triggeredBy("placeOrder()")
}
outputMessage {
sentTo "orders.placed"
body([
orderId: anyUuid(),
userId : anyPositiveInt(),
total : anyDouble()
])
headers {
header("content-type", "application/json")
}
}
}Consumer Side — Stub Runner
@AutoConfigureStubRunner loads the contract-generated WireMock stubs automatically:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
@AutoConfigureStubRunner(
stubsMode = StubRunnerProperties.StubsMode.LOCAL,
ids = {
// groupId:artifactId:version:classifier:port
"com.example:user-service:+:stubs:8091",
"com.example:order-service:+:stubs:8092"
}
)
class BffContractTest {
@Autowired
private WebTestClient webTestClient;
@Test
void shouldAggregateUsingContractDefinedStubs() {
// WireMock is running on 8091 with user-service stubs
// WireMock is running on 8092 with order-service stubs
// application-test.yml points services at those ports
webTestClient.get()
.uri("/api/dashboard/42")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.user.name").isEqualTo("Jane Doe");
}
}Stub modes:
| Mode | Where stubs come from |
|---|---|
LOCAL | Local Maven ~/.m2 repo — provider must ./gradlew publishToMavenLocal first |
REMOTE | Remote Maven/Artifact registry (Nexus, JFrog) |
CLASSPATH | Stubs bundled in the test classpath |
FILE | Local filesystem path |
Provider Side — Verification
The Spring Cloud Contract Gradle plugin generates test classes. The provider team writes a base class that starts the application:
package com.example.userservice.contract;
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest
public abstract class ContractVerifierBase {
@Autowired
private WebApplicationContext context;
@BeforeEach
void setup() {
RestAssuredMockMvc.webAppContextSetup(context);
}
}The plugin generates a test method for each contract:
// Auto-generated — do not edit
public class UserContractVerificationTests extends ContractVerifierBase {
@Test
public void validate_get_user_by_id() throws Exception {
// sends GET /users/42 with Authorization header
// verifies 200, JSON body matches contract
}
}Stub Publishing Workflow
The provider's CI pipeline:
./gradlew test # runs contract verification
./gradlew publishStubs # publishes stub JAR to artifact registry
The consumer's CI pipeline:
./gradlew test # stub runner pulls stubs, runs integration tests
Consumer-Driven vs. Provider-Driven
| Aspect | Consumer-Driven | Provider-Driven (OpenAPI) |
|---|---|---|
| Who writes contracts | Consumer | Provider |
| Drift detection | Automatic (CI fails) | Manual (consumer notices breakage) |
| Provider agility | Lower (must satisfy contracts) | Higher (unilateral changes) |
| Consumer trust | High (your contracts) | Lower (hope provider is accurate) |
| Tooling | Spring Cloud Contract, Pact | OpenAPI Generator |
Consumer-driven is preferred in high-trust, bounded teams. For public APIs or third-party providers, OpenAPI is more practical.
Trade-offs and Limitations
- Requires coordination: contracts live in consumer repo, provider must access them
- Increases coupling between service teams (intentionally)
- Contract management overhead grows with many consumer-provider pairs
- Not suitable for testing complex business logic in the provider — only API shape
- Messaging contracts require additional setup (test message broker)
BFF as Consumer
The BFF-Pattern is a natural consumer-driven contract testing candidate:
- BFF is the sole consumer of its downstream services (in a BFF-per-frontend model)
- BFF team knows exactly what fields it needs — ideal for writing precise contracts
- Contracts serve dual purpose: WireMock stubs for BFF integration tests + provider verification
See P5-BFF-Observability-Testing — TEST-03 for complete BFF contract examples.
Related Concepts
- WireMock — HTTP stub server; stubs generated from contracts
- BFF-Pattern — BFF as consumer in consumer-driven contracts
- BFF-For-SPA — the SPA-specific BFF that benefits from this testing approach
- Spring-Cloud-Gateway — the BFF gateway layer under test