Spring Cloud Contract: Contract Testing for Java Microservices

Spring Cloud Contract: Contract Testing for Java Microservices

Spring Cloud Contract is the contract testing framework for Java Spring applications. Unlike Pact (which is consumer-driven), Spring Cloud Contract is provider-driven — the provider defines contracts in the Groovy DSL, generates WireMock stubs from them, and consumers use those stubs in their tests. This guide shows how to implement Spring Cloud Contract in a Java microservice architecture.

Provider-Driven vs. Consumer-Driven Contracts

Pact (consumer-driven): Consumers write the contracts. Providers verify them. Consumers control what the provider must support.

Spring Cloud Contract (provider-driven): Providers write the contracts. Consumers use auto-generated stubs. Providers control what they guarantee to consumers.

Both approaches work. Spring Cloud Contract fits better when:

  • The provider is a well-defined API that multiple consumers depend on
  • The provider team writes the contracts alongside their implementation
  • You're in a Java/Spring ecosystem and want tight Maven/Gradle integration

Pact fits better when:

  • Consumers are diverse (multiple languages, multiple teams)
  • You want consumers to drive what the provider must implement
  • You need a language-agnostic broker

Project Setup

Provider (Spring Boot application):

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-contract-verifier</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-contract-maven-plugin</artifactId>
            <extensions>true</extensions>
            <configuration>
                <testFramework>JUNIT5</testFramework>
                <baseClassForTests>
                    com.example.userservice.ContractTestBase
                </baseClassForTests>
            </configuration>
        </plugin>
    </plugins>
</build>

Consumer (another Spring Boot application):

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Writing Contracts (Provider Side)

Contracts live in src/test/resources/contracts/ in the provider project. Use the Groovy DSL:

// src/test/resources/contracts/users/get_user_by_id.groovy
import org.springframework.cloud.contract.spec.Contract

Contract.make {
    description "Get user by ID - user exists"
    
    request {
        method 'GET'
        url '/users/123'
        headers {
            accept('application/json')
        }
    }
    
    response {
        status 200
        headers {
            contentType('application/json')
        }
        body([
            id: 123,
            name: "Alice Smith",
            email: "alice@example.com",
            active: true
        ])
    }
}

For dynamic values, use matchers:

Contract.make {
    description "Get any existing user"
    
    request {
        method 'GET'
        url $(consumer(regex('/users/[0-9]+')), producer('/users/123'))
    }
    
    response {
        status 200
        body([
            id: $(producer(123), consumer(anyPositiveInt())),
            name: $(anyNonBlankString()),
            email: $(consumer(anyEmail()), producer('alice@example.com')),
            active: $(anyBoolean())
        ])
        headers {
            contentType('application/json')
        }
    }
}

consumer() specifies what the generated WireMock stub uses. producer() specifies what the provider verification test uses.

For POST requests with request bodies:

Contract.make {
    description "Create a new user"
    
    request {
        method 'POST'
        url '/users'
        headers {
            contentType('application/json')
        }
        body([
            name: $(anyNonBlankString()),
            email: $(consumer(anyEmail()), producer('newuser@example.com'))
        ])
        bodyMatchers {
            jsonPath('$.name', byType())
            jsonPath('$.email', byRegex(email()))
        }
    }
    
    response {
        status 201
        headers {
            contentType('application/json')
        }
        body([
            id: $(anyPositiveInt()),
            name: fromRequest().body('$.name'),
            email: fromRequest().body('$.email')
        ])
    }
}

Provider Test Setup

Spring Cloud Contract generates JUnit 5 tests from contracts automatically. You provide a base test class with setup:

// src/test/java/com/example/userservice/ContractTestBase.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension.class)
public abstract class ContractTestBase {

    @LocalServerPort
    int port;

    @Autowired
    UserRepository userRepository;

    @BeforeEach
    void setup() {
        RestAssured.port = port;

        // Set up test data that contracts expect
        userRepository.deleteAll();
        userRepository.save(new User(123L, "Alice Smith", "alice@example.com", true));
    }
}

Running mvn test in the provider project:

  1. Reads contracts from src/test/resources/contracts/
  2. Generates test classes that extend your base class
  3. Executes the tests against your running Spring Boot app
  4. Verifies response matches contract

Generated test (for reference — you don't edit this):

// target/generated-test-sources/contracts/com/example/ContractVerifierTest.java
public class ContractVerifierTest extends ContractTestBase {

    @Test
    public void validate_get_user_by_id() throws Exception {
        // given
        MockMvcRequestSpecification request = given()
            .header("Accept", "application/json");

        // when
        ResponseOptions response = given().spec(request)
            .get("/users/123");

        // then
        assertThat(response.statusCode()).isEqualTo(200);
        assertThat(response.header("Content-Type")).matches("application/json.*");
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
        assertThatJson(parsedJson).field("['id']").isEqualTo(123);
        assertThatJson(parsedJson).field("['name']").isEqualTo("Alice Smith");
        assertThatJson(parsedJson).field("['email']").isEqualTo("alice@example.com");
    }
}

Publishing Stubs (Maven)

After contracts are verified, publish the generated stubs to a Maven repository:

mvn install  # Installs stubs to local Maven repo
<span class="hljs-comment"># or
mvn deploy   <span class="hljs-comment"># Publishes to remote repository (Nexus, Artifactory)

The plugin generates a user-service-stubs.jar containing WireMock stubs for each contract.

Consumer Tests Using Stubs

Consumers use @AutoConfigureStubRunner to automatically start stub servers:

// order-service/src/test/java/com/example/orderservice/UserClientTest.java
@SpringBootTest
@AutoConfigureStubRunner(
    ids = "com.example:user-service:+:stubs:8080",
    stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
class UserClientTest {

    @Autowired
    UserClient userClient;

    @Test
    void getUser_returnsUserData() {
        // WireMock stub running on port 8080 from user-service stubs
        User user = userClient.getUser(123);

        assertThat(user.getId()).isEqualTo(123);
        assertThat(user.getName()).isNotBlank();
        assertThat(user.getEmail()).contains("@");
    }

    @Test
    void getUser_notFound_throwsException() {
        // The 404 stub was generated from the contract
        assertThatThrownBy(() -> userClient.getUser(999))
            .isInstanceOf(UserNotFoundException.class);
    }
}

ids format: groupId:artifactId:version:classifier:port

For CI, use REMOTE mode to fetch stubs from the artifact repository:

@AutoConfigureStubRunner(
    ids = "com.example:user-service:+:stubs:8080",
    stubsMode = StubRunnerProperties.StubsMode.REMOTE,
    repositoryRoot = "https://nexus.example.com/repository/maven-snapshots/"
)

YAML Contract Alternative

If Groovy DSL feels heavy, Spring Cloud Contract also supports YAML:

# src/test/resources/contracts/users/get_user.yml
description: "Get user by ID"
request:
  method: GET
  url: /users/123
  headers:
    Accept: application/json
response:
  status: 200
  headers:
    Content-Type: application/json
  body:
    id: 123
    name: Alice Smith
    email: alice@example.com
  matchers:
    body:
      - path: $.id
        type: by_type
      - path: $.name
        type: by_regex
        value: ".+"
      - path: $.email
        type: by_regex
        value: ".+@.+"

YAML contracts are easier for non-Java developers to read and write.

Messaging Contracts

Spring Cloud Contract supports message-based interactions (Kafka, RabbitMQ) in addition to REST:

Contract.make {
    description "Order created event"
    
    label 'order_created'
    
    input {
        triggeredBy('createOrder()')
    }
    
    outputMessage {
        sentTo('order-events')
        body([
            orderId: $(anyPositiveInt()),
            userId: $(anyPositiveInt()),
            total: $(anyPositiveDecimalNumber()),
            status: 'CREATED'
        ])
        headers {
            header('contentType', 'application/json')
        }
    }
}

This verifies that when createOrder() is called on the provider, it publishes a message matching the contract to the order-events topic.

CI/CD Integration

# Provider pipeline
provider-contract-tests:
  stage: test
  script:
    - mvn test  # Runs contract verifier tests
    - mvn deploy -DskipTests  # Publishes stubs to artifact repo

# Consumer pipeline
consumer-integration-tests:
  stage: test
  script:
    - mvn test  # Uses stubs from artifact repo via @AutoConfigureStubRunner

Comparing with Pact for Java

Spring Cloud Contract also has a Pact integration — you can write contracts in Pact format and use the Pact Broker while still in the Spring ecosystem. This is useful when you have polyglot microservices and need a language-agnostic broker.

Spring Cloud Contract Pact
Driven by Provider Consumer
Contract format Groovy DSL / YAML JSON (pact file)
Stub format WireMock JSON N/A (mock embedded)
Distribution Maven artifact Pact Broker
Messaging Native (Kafka, AMQP) Limited
Language support JVM-first Polyglot

For pure Java Spring shops, Spring Cloud Contract has tighter integration. For polyglot architectures, Pact with a broker is more flexible.

Summary

Spring Cloud Contract workflow:

  1. Provider writes contracts in Groovy DSL or YAML in src/test/resources/contracts/
  2. Maven plugin generates and runs tests against the provider implementation
  3. Stubs published as Maven artifact after verification
  4. Consumers use @AutoConfigureStubRunner to load stubs from the artifact repository
  5. Consumer tests run against the WireMock stub without a real provider

The Maven artifact approach makes Spring Cloud Contract work naturally in Java CI/CD pipelines — stubs version alongside the service, consumers specify the version they depend on, and incompatibilities are caught when the build fails to pull or verify stubs.

Read more