Back to Articles
Testing StrategiesIntermediate

Contract Testing with Pact

Implement consumer-driven contract testing to ensure API compatibility across microservices

4 min read
...
contract-testingpactmicroservicesapi-testing
Banner for Contract Testing with Pact

Introduction

Contract testing ensures that services can communicate effectively without breaking changes. Learn how to implement Pact for consumer-driven contract testing.

What is Contract Testing?

Contract testing verifies that:

  • Consumer expects certain API behavior
  • Provider delivers that behavior
  • Both parties agree on the contract

Problem It Solves

Without contract testing:

Consumer: "I expect field 'userId'"
Provider: "I renamed it to 'user_id'"
Result: 💥 Integration breaks

With contract testing:

Consumer: Defines contract expecting 'userId'
Provider: Tests against contract
Result: ✅ Breaking change detected early

Pact Basics

The Pact Workflow

  1. Consumer writes tests defining expected interactions
  2. Pact generates contract file (JSON)
  3. Contract is shared with provider
  4. Provider tests against the contract
  5. Results verified - both sides pass

Consumer Side Testing

Setup (Maven)

<dependency>
    <groupId>au.com.dius.pact.consumer</groupId>
    <artifactId>junit5</artifactId>
    <version>4.5.0</version>
    <scope>test</scope>
</dependency>

Writing Consumer Tests

@ExtendWith(PactConsumerTestExt.class)
public class OrderServiceConsumerTest {
    
    @Pact(consumer = "OrderService", provider = "UserService")
    public RequestResponsePact createPact(PactDslWithProvider builder) {
        return builder
            .given("user exists")
            .uponReceiving("a request for user details")
            .path("/users/123")
            .method("GET")
            .willRespondWith()
            .status(200)
            .body(newJsonBody(body -> {
                body.stringType("userId", "123");
                body.stringType("name", "John Doe");
                body.stringType("email", "john@example.com");
            }).build())
            .toPact();
    }
    
    @Test
    @PactTestFor(pactMethod = "createPact")
    void testGetUser(MockServer mockServer) {
        UserClient client = new UserClient(mockServer.getUrl());
        User user = client.getUser("123");
        
        assertEquals("123", user.getUserId());
        assertEquals("John Doe", user.getName());
    }
}

Provider Side Testing

Setup

<dependency>
    <groupId>au.com.dius.pact.provider</groupId>
    <artifactId>junit5</artifactId>
    <version>4.5.0</version>
    <scope>test</scope>
</dependency>

Verifying Contracts

@Provider("UserService")
@PactFolder("pacts")
public class UserServiceProviderTest {
    
    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider.class)
    void pactVerificationTestTemplate(PactVerificationContext context) {
        context.verifyInteraction();
    }
    
    @BeforeEach
    void setUp(PactVerificationContext context) {
        context.setTarget(new HttpTestTarget("localhost", 8080));
    }
    
    @State("user exists")
    void userExists() {
        // Setup test data
        userRepository.save(new User("123", "John Doe", "john@example.com"));
    }
}

Advanced Scenarios

Request Matching

.body(newJsonBody(body -> {
    body.stringType("orderId");  // Must be string
    body.numberType("amount");   // Must be number
    body.array("items", items -> {
        items.object(item -> {
            item.stringType("productId");
            item.integerType("quantity");
        });
    });
}).build())

Provider States

@State("order exists with id 123")
void orderExists() {
    Order order = new Order("123", "CUST1", 100.00);
    orderRepository.save(order);
}
 
@State("order does not exist")
void orderNotFound() {
    orderRepository.deleteAll();
}

Headers and Authentication

.headers("Authorization", "Bearer token123")
.willRespondWith()
.headers("Content-Type", "application/json")

Pact Broker

Why Use Pact Broker?

  • Central contract repository
  • Version management
  • Webhook integration
  • Can-I-Deploy verification

Publishing Contracts

// In pom.xml
<plugin>
    <groupId>au.com.dius.pact.provider</groupId>
    <artifactId>maven</artifactId>
    <configuration>
        <pactBrokerUrl>https://pact-broker.example.com</pactBrokerUrl>
        <pactBrokerToken>${env.PACT_BROKER_TOKEN}</pactBrokerToken>
    </configuration>
</plugin>
mvn pact:publish

CI/CD Integration

Consumer Pipeline

# .github/workflows/consumer-tests.yml
- name: Run Pact Tests
  run: mvn test
  
- name: Publish Contracts
  run: mvn pact:publish
  env:
    PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}

Provider Pipeline

# .github/workflows/provider-verification.yml
- name: Verify Contracts
  run: mvn pact:verify
  
- name: Can I Deploy?
  run: |
    pact-broker can-i-deploy \
      --pacticipant UserService \
      --version ${{ github.sha }} \
      --to production

Best Practices

  1. Test Data Management

    • Use provider states effectively
    • Clean up between tests
    • Use realistic test data
  2. Contract Evolution

    • Add fields (backward compatible)
    • Deprecate before removing
    • Version your APIs
  3. Matching Rules

    • Use type matching, not exact values
    • Match arrays properly
    • Handle optional fields
  4. Organization

    • One pact per provider
    • Group related interactions
    • Name states clearly

Common Pitfalls

Over-Specifying Contracts

Bad:

body.stringValue("email", "john@example.com")  // Exact match

Good:

body.stringType("email", "john@example.com")   // Type match

Forgetting Provider States

Bad:

// No @State, test data might not exist

Good:

@State("product in stock")
void productInStock() {
    productRepository.save(new Product("PROD1", 10));
}

Tools and Resources

Conclusion

Contract testing with Pact enables confident, independent service development. Start with critical service interactions and expand coverage over time.

Part of the QE Hub Testing Strategies series.

Comments (0)

Loading comments...