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 with id, 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 in target/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:

  1. Ensure the consumer contract is in target/pacts.
  2. 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 and mvn 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.
Stay tuned for more testing tutorials!