Automating Contract Testing: A Developer’s Guide with Spring Cloud Contract
Introduction
In Microservices architecture, seamless communication between services is essential. As systems grow in complexity, traditional integration testing often struggles to catch issues early. This is where Contract Testing becomes invaluable.
This guide explores Spring Cloud Contract, a robust framework for implementing contract testing in Java-based microservices. Whether you're a beginner or looking to enhance your skills, you'll find practical insights, examples, and best practices to master contract testing.
What is Contract Testing?
Contract Testing is a testing approach that focuses on verifying the interactions between service providers and consumers. It ensures that both parties adhere to the agreed-upon behavior, reducing integration issues and improving collaboration, even when developed independently.
Why Spring Cloud Contract?
Spring Cloud Contract simplifies contract testing by providing a full-fledged framework to define, verify, and share contracts between service providers and consumers.
Key features of Spring Cloud Contract include:
- Automation: Automatically generates tests and stubs from contracts, saving time and effort.
- Consumer-Driven Contracts: Supports a consumer-first approach, ensuring APIs meet consumer expectations.
- Stubbing: Generates stubs for service providers, allowing consumers to test against a mocked version of the API.
- Integration with Spring: Seamlessly integrates with Spring Boot and other Spring projects, making it easy to adopt in existing applications.
- Flexibility: Supports multiple contract formats, including Java, Groovy DSL and YAML, catering to different team preferences.
- CI/CD Friendly: Easily integrates into CI/CD pipelines, enabling continuous verification of contracts.
How Contract Testing Works ?
Spring Cloud Contract ensures seamless contract testing by automating the verification of interactions between service providers and consumers.
Here's the workflow:
sequenceDiagram
participant Provider as Provider Service
participant Artifactory as Artifactory
participant Consumer as Consumer Service
Provider->>Provider: Define Contracts (YAML/Groovy DSL)
Provider->>Provider: Generate Tests from Contracts
Provider->>Provider: Run Tests to Verify Contracts
Provider->>Provider: Generate Stubs
Provider->>Artifactory: Publish Stubs
Consumer->>Artifactory: Fetch Stubs
Consumer->>Consumer: Test Integration with Stubs
- Define Contracts: Write contracts in Groovy DSL or YAML format specifying request-response expectations.
- Generate Tests: Spring Cloud Contract generates provider-side tests from the contracts.
- Run Tests: Execute the generated tests to ensure the provider adheres to the contract.
- Generate Stubs: Stubs are created for consumers to simulate provider behavior.
- Publish Stubs: The provider publishes the generated stubs to an artifact repository.
- Fetch Stubs: The consumer fetches the stubs from the artifact repository.
- Consumer Testing: Consumers use the stubs to test their integration without relying on the actual provider.
Provider Service
Project Setup
To get started with Spring Cloud Contract, you need to configure your project with the necessary dependencies and plugins. Below is a step-by-step guide to set up your project.
1. Add Maven Dependencies
Include the Spring Cloud Contract dependencies in your pom.xml file. These dependencies provide the core functionality for contract testing.
<project>
...
<properties>
<spring-cloud-contract.version>4.2.1</spring-cloud-contract.version>
</properties>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-contract.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
...
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<version>${spring-cloud-contract.version}</version>
</dependency>
...
</project>
2. Configure the Spring Cloud Contract Plugin
Add the Spring Cloud Contract Maven plugin to your pom.xml
. This plugin is responsible for generating test classes and
stubs from your contract files.
<build>
<plugins>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<baseClassForTests>com.github.nramc.dev.journey.api.JourneyApiContractBase</baseClassForTests>
</configuration>
</plugin>
</plugins>
</build>
Note
Please refer to the Spring Cloud Contract Maven Plugin to know more about the configuration options available for the plugin.
3. Define a Base Test Class
Create a base test class for your contract tests. This class will set up the necessary context for your tests and configure RestAssured to use the Spring Web Application context.
package com.github.nramc.dev.journey.api;
import com.github.nramc.dev.journey.api.config.TestContainersConfiguration;
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.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest(classes = {JourneyApiApplication.class})
@Import(TestContainersConfiguration.class)
@ActiveProfiles("test")
// Suppressing the warning for the test class as public visibility is required for contract tests
@SuppressWarnings("java:S5786")
public class JourneyApiContractBase {
@Autowired
WebApplicationContext context;
@BeforeEach
void setup() {
RestAssuredMockMvc.webAppContextSetup(this.context);
}
}
Note
The JourneyApiContractBase
class use @SpringBootTest
and @ActiveProfiles
annotations to load the complete
Spring application context. This is not always required to perform contract tests. We can use @MockMvcTest
to load
only the required beans for the test. In the above example, we are using @SpringBootTest
to load the complete
application context with testcontainer to test the complete API flow.
4. Run Maven Command
Execute the following Maven command to generate the contract tests and stubs:
Note: Build might fail if you have not defined any contract files. You can skip the build by using the -DskipTests
flag.
This will generate test classes in the target/generated-test-sources/contracts
directory and stubs in the
target/stubs
directory.
With this setup, your project is ready to use Spring Cloud Contract for contract testing.
Defining Contract
Contracts define the expected interactions between service providers and consumers. In Spring Cloud Contract, these contracts can be written in Groovy DSL or YAML format. They specify the request and response details for API endpoints, ensuring both parties adhere to the agreed behavior.
1. Create the Contract File
Contracts are typically placed in the src/test/resources/contracts
directory. Below is an example of a contract
written in YAML format for a signup API endpoint:
request:
method: POST
url: /rest/signup
body:
"username": example-username@example.com
"password": Strong@password123
"name": "John Doe"
headers:
Content-Type: application/json
matchers:
body:
- path: $.['username']
type: by_regex
value: "[a-zA-Z0-9._-]{8,20}@[a-zA-Z0-9]{3,20}\\.[a-zA-Z]{2,6}"
- path: $.['password']
type: by_regex
value: "(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@.#$!%*?&^])[A-Za-z\\d@.#$!%*?&]{8,50}"
- path: $.['name']
type: by_regex
value: "[a-zA-Z\\s]{3,50}"
response:
status: 201
2. Contract File Structure
- Request: Defines the HTTP method, URL, headers, and body.
- Matchers: Validates the request body using patterns (e.g., regex).
- Response: Specifies the expected status, headers, and body.
Tip
Please refer to the Spring Cloud Contract's YML Schema for detailed information on the DSL syntax and available options.
3. Organizing Contracts
Organize contracts by grouping them into subdirectories based on functionality or API endpoints.
For example
src/test/resources/contracts/
/signup/
- signup-contract.yaml
- signup-invalid-request.yaml
/login/
- login-contract.yaml
- login-invalid-request.yaml
/userinfo/
- userinfo-contract.yaml
- userinfo-invalid-request.yaml
By defining contracts, you ensure clear communication between services and enable automated testing of API interactions.
Generate and Verify Tests
Once you have defined the contract files, you can generate the test classes and stub files using the Spring Cloud Contract Maven plugin. The plugin will read the contract files and generate the test classes and stub files based on the contract definitions.
When you run the Maven command:
It will generate the test class ContractVerifierTest
in the target/generated-test-sources/contracts
directory. The
test class will contain the contract tests for the API endpoints defined in the contract files. The generated test class
will use RestAssured to perform the API calls and assert the responses.
package com.github.nramc.dev.journey.api;
import com.github.nramc.dev.journey.api.JourneyApiContractBase;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
import io.restassured.response.ResponseOptions;
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static io.restassured.module.mockmvc.RestAssuredMockMvc.*;
@SuppressWarnings("rawtypes")
public class ContractVerifierTest extends JourneyApiContractBase {
@Test
public void validate_signup_contract() throws Exception {
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/json")
.body("{\"username\":\"example-username@example.com\",\"password\":\"Strong@password123\",\"name\":\"John Doe\"}");
// when:
ResponseOptions response = given().spec(request)
.post("/rest/signup");
// then:
assertThat(response.statusCode()).isEqualTo(201);
}
}
Generate & Publish Stubs
Maven build command mvn install
will also generate stub files in journey-api-web/target/stubs
directory for
the API endpoints defined in the contract files.
The stub files will be in JSON format and contain the request and response details for the API endpoints.
These stub files packaged as jar files journey-api-web/target/journey-api-web-1.0.1-SNAPSHOT-stubs.jar
and published
to the artifactory repository. These stub files can then be reused by other microservices to mock API responses during
testing.
Please find below the sample stub file
journey-api-web/target/stubs/META-INF/com.github.nramc.dev.journey/journey-api-web/1.0.1-SNAPSHOT/mappings/signup-contract.json
generated for the signup API endpoint.
{
"id": "9b4e7581-0d7c-4ba2-af50-f84efa624991",
"request": {
"urlPath": "/rest/signup",
"method": "POST",
"headers": {
"Content-Type": {
"equalTo": "application/json"
}
},
"bodyPatterns": [
{
"matchesJsonPath": "$[?(@.['username'] =~ /([a-zA-Z0-9]{8,20}@[a-zA-Z0-9]{3,20}\\.[a-zA-Z]{2,6})/)]"
},
{
"matchesJsonPath": "$[?(@.['password'] =~ /((?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@.#$!%*?&^])[A-Za-z\\d@.#$!%*?&]{8,50})/)]"
},
{
"matchesJsonPath": "$[?(@.['name'] =~ /([a-zA-Z\\s]{3,50})/)]"
}
]
},
"response": {
"status": 201,
"transformers": [
"response-template",
"spring-cloud-contract"
]
},
"uuid": "9b4e7581-0d7c-4ba2-af50-f84efa624991"
}
Consumer Service
The consumer microservice will use the stub files generated by provider to mock the API responses. The consumer microservice will use WireMock to mock the API responses using the stub files.
Project Setup
To set up the consumer microservice with Spring Cloud Contract, you need to add the following dependencies in your
pom.xml
file.
<project>
...
<properties>
<spring-cloud-contract.version>4.0.0</spring-cloud-contract.version>
</properties>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-contract.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
...
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
...
</project>
Fetching Stubs
The stub files can be fetched from the artifactory repository using the Spring Cloud Contract Stub Runner.
There are two modes to fetch the stub files,
- Local Mode: The stub files are downloaded from the local directory.
- Remote Mode: The stub files are downloaded from the artifactory repository.
In this example, we will use the local mode to fetch the stub files.
Include the stub JAR in your pom.xml
under the test scope,
<dependency>
<groupId>com.github.nramc.dev.journey</groupId>
<artifactId>journey-api-web</artifactId>
<version>${journey-api.version}</version>
<classifier>stubs</classifier>
<scope>test</scope>
</dependency>
This will include the stub files in the classpath of the consumer service.
Configure Stub Runner
The Stub Runner in Spring Cloud Contract allows you to fetch the stub files from the local directory or remote repository and use them to mock the API responses.
There are various ways to configure the Stub Runner in your consumer service.
1. Using @AutoConfigureStubRunner
annotation
You can use the @AutoConfigureStubRunner
annotation to configure the Stub Runner in your test class. This will
automatically download the stub files from the local directory or remote repository and use them to mock the API
responses.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = JourneyIntegrationApplication.class)
@ActiveProfiles("integration") // application specific profile to load context
@AutoConfigureStubRunner(
ids = "com.github.nramc.dev.journey:journey-api-web:+:stubs:6565",
stubsMode = StubRunnerProperties.StubsMode.LOCAL)
class JourneyApiContractsTest {
// Test cases will be here
}
2. Using JUnit5 Extension
You can also use the StubRunnerExtension
to configure the Stub Runner in your test class. This will allow you to
configure the Stub Runner in a more flexible way and use it in your test class.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = JourneyIntegrationApplication.class)
@ActiveProfiles("integration") // application specific profile to load context
class JourneyApiContractsTest {
@RegisterExtension
public static StubRunnerExtension stubRunnerExtension = new StubRunnerExtension()
.downloadStub("com.github.nramc.dev.journey:journey-api-web:+:stubs:6565")
.stubsMode(StubRunnerProperties.StubsMode.LOCAL);
// Test cases will be here
}
Note
To use the remote mode, you need to set the stubsMode
to REMOTE
and provide the repositoryRoot
URL.
Please refer to the Spring Cloud Contract Stub Runner
Writing Tests
Once you have configured the Stub Runner, you can write the consumer tests using RestAssured to perform the API calls and assert the responses.
package com.github.nramc.dev.journey.api.tests.testcase.contracts;
import com.github.nramc.dev.journey.api.tests.application.JourneyIntegrationApplication;
import io.restassured.RestAssured;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = JourneyIntegrationApplication.class)
@ActiveProfiles("integration") // application specific profile to load context
@AutoConfigureStubRunner(
ids = "com.github.nramc.dev.journey:journey-api-web:+:stubs:6565",
stubsMode = StubRunnerProperties.StubsMode.LOCAL)
class JourneyApiContractsTest {
@Test
void signup_whenSignupDataValid_shouldReturnSuccess() {
RestAssured.given()
.port(6565)
.contentType("application/json")
.accept("application/json")
.body("""
{"username":"username@example.com", "password":"Strong@password123", "name":"John Doe"}
""")
.post("/rest/signup")
.then()
.statusCode(201);
}
}
Running the Tests
To run the tests, you can use the following command:
This will run the tests in the JourneyApiContractsTest
class and use the stub files to mock the API responses.
Conclusion
In this guide, we explored the fundamentals of contract testing and how to implement it using Spring Cloud Contract.
Contract testing is a game-changer in microservices architecture, ensuring seamless communication between services. By automating contract testing, you can catch integration issues early, improve collaboration between teams, and enhance the overall quality of your software.
You can further enhance your testing strategy by combining contract testing with API schema validation or integrating it into your CI/CD pipeline for continuous feedback.
Strong contracts build strong microservices.
References
Did this post help you? Share on: X (Twitter) Facebook LinkedIn reddit WhatsApp Hacker News