Hire Me
← All Writing Spring Boot

Consumer-Driven Contract Testing with Spring Cloud Contract

Consumer-driven contract testing with Spring Cloud Contract — covering the contract DSL, stub generation, stub runner, and integrating contract verification into CI/CD pipelines.

The hardest integration bugs in microservices are the ones that don’t show up until production. Two teams working in parallel, both confident their services are correct, both with green test suites — and a deployment that breaks because the consumer expected a field called claimReference and the producer renamed it to claimRef in a refactor three weeks ago. End-to-end integration tests catch this, but they’re slow, fragile, and require all services to be deployed simultaneously. Consumer-driven contract testing is the middle ground: fast, service-isolated, and catches API breaking changes before they ship.

Spring Cloud Contract implements this pattern cleanly in the Spring Boot ecosystem.

How Consumer-Driven Contracts Work

The consumer defines what it expects from the producer. That expectation becomes a contract — a formal specification of the API interaction. The producer verifies it can satisfy every contract. The consumer tests against a stub generated from the same contract.

The critical insight: the stub and the real implementation are verified against the same contract file. If the producer verification passes and the consumer stub test passes, they will work together in production. You don’t need them deployed together to know that.

Writing Contracts

Contracts live in the producer’s repository under src/test/resources/contracts/. They can be written in Groovy DSL or YAML. I prefer Groovy for REST contracts — the DSL is concise and handles request/response matching expressively.

A REST contract for an eligibility assessment endpoint:

import org.springframework.cloud.contract.spec.Contract

Contract.make {
    description "Returns ELIGIBLE when a working-age claimant requests assessment"

    request {
        method POST()
        url "/eligibility/assess"
        headers {
            contentType applicationJson()
        }
        body([
            age: 30,
            employmentStatus: "UNEMPLOYED"
        ])
    }

    response {
        status 200
        headers {
            contentType applicationJson()
        }
        body([
            outcome: "ELIGIBLE",
            reasonCode: $(anyNonEmptyString())
        ])
    }
}

The $(anyNonEmptyString()) matcher lets the producer return any non-empty reason code — the contract specifies shape and constraints, not exact values where variation is acceptable.

For Kafka event contracts, the DSL targets a message rather than HTTP:

Contract.make {
    description "ClaimApproved event published when a claim is approved"

    label "claim_approved"

    input {
        triggeredBy("approveClaim()")
    }

    outputMessage {
        sentTo "benefit.claims.approved"
        body([
            claimId: $(anyNonEmptyString()),
            approvedAt: $(anyIso8601WithOffset()),
            assessorId: $(anyNonEmptyString())
        ])
        headers {
            header("contentType", applicationJson())
        }
    }
}

Producer Verification

On the producer side, Spring Cloud Contract generates a base test class from each contract. You provide the base class that sets up the Spring context:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMessageVerifier
public abstract class ContractVerifierBase {

    @Autowired
    private EligibilityController eligibilityController;

    @BeforeEach
    void setup() {
        RestAssuredMockMvc.standaloneSetup(eligibilityController);
    }

    // Method called by Kafka contract test
    public void approveClaim() {
        claimService.approve(new ClaimId("TEST-001"), new AssessorId("ASSESSOR-001"), "Test approval");
    }
}

Run mvn verify. Spring Cloud Contract generates test classes from your contract files, executes them against your running application, and fails the build if any contract is violated. This is the producer side of the guarantee.

The plugin configuration in pom.xml:

<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <extensions>true</extensions>
    <configuration>
        <baseClassForTests>
            com.trinitylogic.eligibility.ContractVerifierBase
        </baseClassForTests>
    </configuration>
</plugin>

Consumer Stub Runner

The producer build publishes stub JARs to your Maven repository. The consumer tests use StubRunner to pull those stubs and run them as local HTTP/messaging fakes:

@SpringBootTest
@AutoConfigureStubRunner(
    ids = "com.trinitylogic:eligibility-service:+:stubs:8090",
    stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
class EligibilityClientTest {

    @Autowired
    private EligibilityClient eligibilityClient;

    @Test
    void shouldReceiveEligibleOutcomeForWorkingAgeClaimant() {
        AssessmentRequest request = AssessmentRequest.builder()
            .age(30)
            .employmentStatus(EmploymentStatus.UNEMPLOYED)
            .build();

        AssessmentResult result = eligibilityClient.assess(request);

        assertThat(result.getOutcome()).isEqualTo(AssessmentOutcome.ELIGIBLE);
    }
}

The + version selector picks the latest published stub. StubsMode.LOCAL looks in your local Maven cache — use REMOTE in CI to pull from your Nexus or Artifactory instance.

The stub responds exactly as the contract specifies. If the consumer test passes and the producer verification passes against the same contract file, the services will work together.

Kafka Contract Consumer Side

For Kafka contracts, the consumer test triggers the label and asserts the event was received:

@SpringBootTest
@AutoConfigureStubRunner(
    ids = "com.trinitylogic:claims-service:+:stubs",
    stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
class ClaimApprovedListenerTest {

    @Autowired
    private StubTrigger stubTrigger;

    @Autowired
    private ProcessedClaimRepository processedClaimRepository;

    @Test
    void shouldProcessClaimApprovedEvent() throws Exception {
        stubTrigger.trigger("claim_approved");

        await().atMost(10, TimeUnit.SECONDS).untilAsserted(() ->
            assertThat(processedClaimRepository.count()).isGreaterThan(0)
        );
    }
}

The StubTrigger fires the Kafka message defined in the contract. Your actual consumer listener receives it and you assert the downstream effect. Real Kafka behaviour, no producer deployment required.

Integrating into CI/CD

The workflow that makes this valuable is:

  1. Consumer team writes or updates a contract file and opens a PR against the producer repository
  2. Producer CI runs contract verification — if it fails, the consumer knows the API doesn’t support the contract yet
  3. Producer ships a feature that satisfies the contract, publishes stubs
  4. Consumer CI runs stub tests against the latest published stubs

This separates two questions cleanly: “does my service work?” (unit and integration tests) and “will my service work with this other service?” (contract tests). Neither question requires both services to be deployed.

For teams using a contract registry like the Pact Broker, Spring Cloud Contract can publish and fetch contracts centrally rather than committing them to the producer repo. This is worth the additional setup once you have more than three or four service pairs with contracts.

ProTips

If you’re looking for a Java contractor who has implemented contract testing across distributed microservices, get in touch.