Introduction
In our previous post, we explored Best Practices for REST Assured, learning how to write robust and maintainable API tests. Now, we’ll extend our REST Assured journey with Contract Testing, a technique to verify compatibility between API consumers and providers using Pact. This guide integrates REST Assured with Pact to create and validate contracts, ensuring reliable microservices communication. It’s designed for beginners and experienced developers, providing clear explanations and practical examples.
Key Point: Contract testing with Pact and REST Assured ensures that API consumers and providers agree on the expected request/response structure, preventing integration issues in distributed systems.
What is Contract Testing?
Contract Testing is a testing approach that verifies the interactions between a consumer (e.g., a client application) and a provider (e.g., an API server) by defining a contract. The contract specifies expected requests and responses, ensuring both parties adhere to the agreed interface.
Pact is a popular contract testing framework that generates contracts from consumer tests and verifies them against the provider. REST Assured is used on the provider side to send requests and validate responses against the contract, ensuring compatibility.
Use cases include:
- Ensuring microservices communicate correctly.
- Testing APIs without relying on live environments.
- Detecting breaking changes in API contracts early.
Setting Up Pact with REST Assured
We’ll set up a consumer-driven contract test where the consumer defines expectations using Pact, and the provider uses REST Assured to verify the contract. We’ll use a mock API for the consumer and https://jsonplaceholder.typicode.com
as a sample provider for demonstration.
Ensure your pom.xml
includes dependencies for REST Assured, JUnit, Pact, and Allure (for reporting):
11
11
4.5.12
2.27.0
1.9.22
io.rest-assured
rest-assured
5.4.0
test
org.junit.jupiter
junit-jupiter
5.10.2
test
org.hamcrest
hamcrest
2.2
test
au.com.dius.pact.consumer
junit5
${pact.version}
test
au.com.dius.pact.provider
junit5
${pact.version}
test
io.qameta.allure
allure-junit5
${allure.version}
test
org.apache.maven.plugins
maven-surefire-plugin
3.2.5
-javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
org.aspectj
aspectjweaver
${aspectj.version}
au.com.dius.pact
pact-jvm-provider-maven
${pact.version}
The Pact dependencies enable consumer and provider testing, while the Pact Maven plugin runs provider tests.
Creating a Consumer Contract with Pact
On the consumer side, we’ll use Pact to define expectations for an API (e.g., retrieving a post). The consumer test generates a contract file.
Create a consumer test in src/test/java/com/example/consumer/PostConsumerTest.java
:
package com.example.consumer;
import au.com.dius.pact.consumer.MockServer;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import au.com.dius.pact.core.model.RequestResponsePact;
import au.com.dius.pact.core.model.annotations.Pact;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "PostProvider")
public class PostConsumerTest {
@Pact(consumer = "PostConsumer")
public RequestResponsePact createPact(PactDslWithProvider builder) {
PactDslJsonBody responseBody = new PactDslJsonBody()
.integerType("id", 1)
.integerType("userId", 1)
.stringType("title", "Test Post")
.stringType("body", "Test Body");
return builder
.given("a post exists with ID 1")
.uponReceiving("a request to get post with ID 1")
.path("/posts/1")
.method("GET")
.willRespondWith()
.status(200)
.body(responseBody)
.toPact();
}
@Test
@PactTestFor
public void testGetPost(MockServer mockServer) {
RestAssured.baseURI = mockServer.getUrl();
Response response = given()
.log().all()
.when()
.get("/posts/1");
response.then()
.statusCode(200)
.body("id", equalTo(1))
.body("title", equalTo("Test Post"));
assertEquals(200, response.getStatusCode());
}
}
Explanation:
@Pact
: Defines the contract, specifying the expected request (GET/posts/1
) and response (JSON withid
,userId
,title
,body
).PactDslJsonBody
: Builds the expected response structure.MockServer
: Runs a mock server provided by Pact, simulating the provider.- REST Assured sends a request to the mock server and validates the response.
- Running
mvn test
generates a contract file intarget/pacts/PostConsumer-PostProvider.json
.
Important: The consumer test defines what the consumer expects from the provider, creating a contract that the provider must satisfy.
Verifying the Contract on the Provider Side
On the provider side, we’ll use REST Assured to verify that the real API satisfies the contract generated by the consumer.
Create a provider test in src/test/java/com/example/provider/PostProviderTest.java
:
package com.example.provider;
import au.com.dius.pact.provider.junit5.HttpTestTarget;
import au.com.dius.pact.provider.junit5.PactVerificationContext;
import au.com.dius.pact.provider.junit5.PactVerificationJUnit5Extension;
import au.com.dius.pact.provider.junitsupport.Provider;
import au.com.dius.pact.provider.junitsupport.State;
import au.com.dius.pact.provider.junitsupport.loader.PactFolder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
@Provider("PostProvider")
@PactFolder("target/pacts")
@ExtendWith(PactVerificationJUnit5Extension.class)
public class PostProviderTest {
@BeforeEach
void setup(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("jsonplaceholder.typicode.com", 443, "", true));
}
@TestTemplate
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
@State("a post exists with ID 1")
public void postExistsState() {
// No setup needed for jsonplaceholder.typicode.com as it already has post ID 1
}
}
Explanation:
@Provider
: Identifies the provider (matches the provider name in the consumer test).@PactFolder
: Points to the directory containing the contract file.HttpTestTarget
: Configures the real provider URL (jsonplaceholder.typicode.com
).@State
: Defines the provider state (no setup needed here since the API already has the data).- Pact sends the contract’s expected request to the provider, and REST Assured (via Pact) validates the response.
Run Provider Tests:
- Ensure the consumer contract is in
target/pacts
. - Run:
mvn pact:verify
.
The test passes if the provider’s response matches the contract.
Pro Tip: For real APIs, use a test database or mock data to set up provider states, ensuring the API is in the correct state for each contract.
Integrating with Allure Reporting
Enhance contract tests with Allure reports for better visibility in CI/CD pipelines.
Update the consumer test to include Allure annotations:
package com.example.consumer;
import au.com.dius.pact.consumer.MockServer;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import au.com.dius.pact.core.model.RequestResponsePact;
import au.com.dius.pact.core.model.annotations.Pact;
import io.qameta.allure.Allure;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;
@Feature("Contract Testing")
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "PostProvider")
public class PostConsumerWithAllureTest {
@Pact(consumer = "PostConsumer")
public RequestResponsePact createPact(PactDslWithProvider builder) {
PactDslJsonBody responseBody = new PactDslJsonBody()
.integerType("id", 1)
.integerType("userId", 1)
.stringType("title", "Test Post")
.stringType("body", "Test Body");
return builder
.given("a post exists with ID 1")
.uponReceiving("a request to get post with ID 1")
.path("/posts/1")
.method("GET")
.willRespondWith()
.status(200)
.body(responseBody)
.toPact();
}
@Test
@PactTestFor
@Description("Verify consumer contract for retrieving a post")
public void testGetPost(MockServer mockServer) {
RestAssured.baseURI = mockServer.getUrl();
Response response = given()
.log().all()
.when()
.get("/posts/1");
Allure.addAttachment("Response Body", "application/json", response.asString(), ".json");
response.then()
.statusCode(200)
.body("id", equalTo(1))
.body("title", equalTo("Test Post"));
assertEquals(200, response.getStatusCode());
}
}
Explanation:
@Feature
and@Description
: Organize and document the test in Allure.Allure.addAttachment
: Attaches the response body to the report.- Run
mvn clean test
andmvn allure:serve
to view the report.
Integrating with CI/CD
Add contract tests to a GitHub Actions pipeline, reusing the setup from the CI/CD post.
Update .github/workflows/ci.yml
:
name: REST Assured CI Pipeline
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'temurin'
- name: Cache Maven dependencies
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-maven-
- name: Run consumer tests
run: mvn clean test
- name: Run provider tests
run: mvn pact:verify
- name: Generate Allure report
run: mvn allure:report
- name: Upload Allure results
if: always()
uses: actions/upload-artifact@v4
with:
name: allure-results
path: target/allure-results
- name: Publish Allure report
if: always()
uses: simple-elf/allure-report-action@v1.7
with:
allure_results: target/allure-results
gh_pages: gh-pages
allure_report: allure-report
- name: Deploy report to GitHub Pages
if: always()
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: allure-report
Explanation:
- Adds
mvn pact:verify
to run provider tests. - Publishes Allure reports for consumer tests.
- Contract files in
target/pacts
are used for provider verification.
Tips for Beginners
- Start with Simple Contracts: Define contracts for basic endpoints before tackling complex interactions.
- Use Pact Broker: For real projects, store contracts in a Pact Broker to share between consumer and provider teams.
- Validate States: Ensure provider states are correctly set up to match contract expectations.
- Enable Logging: Use
log().all()
in REST Assured and Pact’s debug logs to troubleshoot contract mismatches.
Troubleshooting Tip: If provider tests fail, check the contract file for mismatches in paths, headers, or body content. Use Pact’s error messages to identify discrepancies.
What’s Next?
This post extends our REST Assured series with contract testing, a vital skill for microservices environments. To continue your learning, explore topics like:
- Security Testing: Testing API authentication and authorization with REST Assured.
- Advanced Allure Reporting: Customizing reports with additional metadata.
- Other Frameworks: Combining REST Assured with tools like Selenium for end-to-end testing.