CodeGym /Courses /Module 5. Spring /Lecture 267: Hands-on: writing contract tests for microse...

Lecture 267: Hands-on: writing contract tests for microservice interactions

Module 5. Spring
Level 21 , Lesson 6
Available

Imagine you have two microservices: the "Customer" service and the "Product" service. The first one wants to get product info from the second. Sounds simple, but what happens if the team owning the "Product" service decides to change the structure of the returned JSON objects? Yeah — the "Customer" service will start happily breaking. To avoid that mess, we use contracts that help make sure the interaction between services stays "in sync".


What is Pact?

Pact is a contract testing framework that helps ensure two microservices (or a client/server) interact the way they promised. Pact works on a "consumer-provider" model:

  • Consumer (Consumer) creates a contract that describes how it expects the provider to behave.
  • Provider (Provider) verifies that its functionality matches the contract and updates the contract if things change.

Example simple Pact flow:

[Consumer service -> Generates contract -> Contract stored -> Provider verifies contract]

Task: Let's implement contract testing

For our example we'll have:

  1. Consumer — the microservice CustomerService, which calls the API of ProductService.
  2. Provider — the microservice ProductService, which serves product data.

Goal: write tests for CustomerService and make sure ProductService conforms to the contract.

Step 1: Prepare the environment

To work with Pact in Gradle add the following dependencies:


dependencies {
    testImplementation 'au.com.dius.pact.provider:junit5:4.6.2'
    testImplementation 'au.com.dius.pact.consumer:junit5:4.6.2'
}

Now we have everything to work with Pact on both the consumer and provider sides.

Step 2: Write a contract for the consumer

Create a test class CustomerServicePactTest where we'll describe the interaction with ProductService.


import au.com.dius.pact.consumer.Pact;
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 org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;

@ExtendWith(PactConsumerTestExt.class)
public class CustomerServicePactTest {

    @Pact(consumer = "CustomerService", provider = "ProductService")
    public Map<String, Object> createPact(PactDslWithProvider builder) {
        return builder
                .given("Product with ID 1 exists")
                .uponReceiving("request to get product info")
                .path("/products/1")
                .method("GET")
                .willRespondWith()
                .status(200)
                .body("{\"id\": 1, \"name\": \"Laptop\", \"price\": 999.99}")
                .toPact();
    }

    @Test
    @PactTestFor(providerName = "ProductService", port = "8080")
    void testGetProduct() {
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> response = restTemplate.getForEntity("http://localhost:8080/products/1", String.class);

        assertEquals(200, response.getStatusCodeValue());
        assertEquals("{\"id\": 1, \"name\": \"Laptop\", \"price\": 999.99}", response.getBody());
    }
}

What's going on here:

  1. In the createPact method we describe the contract:
    • Given that a Product with ID 1 exists
    • If the client requests /products/1, return a JSON object with product data
  2. In the testGetProduct test we check that CustomerService sends the request correctly and gets the expected response.

This test generates a contract that is written out as a JSON file.

Example of the contract:


{
  "provider": { "name": "ProductService" },
  "consumer": { "name": "CustomerService" },
  "interactions": [
    {
      "description": "request to get product info",
      "request": {
        "method": "GET",
        "path": "/products/1"
      },
      "response": {
        "status": 200,
        "body": { "id": 1, "name": "Laptop", "price": 999.99 }
      }
    }
  ]
}

Step 3: Verify the contract on the provider side

Now move to ProductService. Here we'll make sure the API matches the contract.

Create a test class ProductServicePactTest:


import au.com.dius.pact.provider.junit5.PactVerificationContext;
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(PactVerificationInvocationContextProvider.class)
public class ProductServicePactTest {

    @BeforeEach
    void before(PactVerificationContext context) {
        context.setTarget(new HttpTestTarget("localhost", 8080));
    }

    @Test
    void pactVerificationTest(PactVerificationContext context) {
        context.verifyInteraction();
    }
}

The key points here:

  • HttpTestTarget tells where our provider API is hosted (localhost:8080).
  • The verifyInteraction method loads the contract we generated for the consumer and checks that ProductService satisfies the described interactions.

Step 4: Run everything together

  1. Bring up ProductService on localhost:8080. Make sure it returns the correct data for /products/1.
  2. Run the CustomerServicePactTest first to generate the contract.
  3. Then run ProductServicePactTest to verify the provider.

Error handling and common issues

  1. Provider API changes: if the ProductService team changes the API, provider tests will start failing. That's a signal — update the contract and coordinate the change with the consumer.
  2. Data mismatch in the contract: if the provider returns extra fields, that's not necessarily a failure, but you should discuss it with the consumer team.
  3. Multiple contracts: if ProductService serves multiple consumers, each consumer's contract should be verified separately.

Why this matters in practice

Contract testing helps catch integration issues between microservices before they hit production. This is especially important for teams working on different services, where changes on one side can silently "break" the other side.

Contracts are also great for CI/CD: you can run these tests as part of your build pipeline to automate verification of changes.


Congrats! Now you know how to write Pact contract tests. This is an important step toward reliable and robust microservice systems. Here's to stable integrations!

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION