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

RoleDescriptionWritesRuns
ConsumerService that calls the API (e.g., BFF calling User Service)ContractsConsumer integration tests using generated stubs
ProviderService 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:

ModeWhere stubs come from
LOCALLocal Maven ~/.m2 repo — provider must ./gradlew publishToMavenLocal first
REMOTERemote Maven/Artifact registry (Nexus, JFrog)
CLASSPATHStubs bundled in the test classpath
FILELocal 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

AspectConsumer-DrivenProvider-Driven (OpenAPI)
Who writes contractsConsumerProvider
Drift detectionAutomatic (CI fails)Manual (consumer notices breakage)
Provider agilityLower (must satisfy contracts)Higher (unilateral changes)
Consumer trustHigh (your contracts)Lower (hope provider is accurate)
ToolingSpring Cloud Contract, PactOpenAPI 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.

  • 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