top of page

21 November 2023

7 Min. Read

Contract Testing

Contract Testing for Microservices: A Complete Guide

Consumer-driven Contract Testing with PACT A Step-by-Step Guide.jpg

Key Highlights

In this blog, we cover the following key highlights:

  1. Define precise contracts for expected interactions between services.

  2. Ensure thorough test coverage to validate contract adherence.

  3. Implement automated contract tests for streamlined validation.

  4. Integrate contract testing into the CI/CD pipeline for continuous validation and prevention of regressions.

This guide introduces a proactive testing strategy, optimizing microservices benefits while avoiding interdependency pitfalls.

Everybody’s doing microservices now-a-days, but the teams keeps getting the same challenges over and over again and what they are really struggling with is:


I switched to microservices to boost my business agility, but now I'm in a bit of a bind. I can make sure each piece of the puzzle works fine, but when it comes to ensuring the whole system runs smoothly, I'm kind of lost. Testing everything together ends up erasing most of the benefits of using microservices. It turns into a real headache – it's tough, expensive, and just plain annoying to set up.

-Holly Cummins, Quarkus

Companies, like Uber, even planned to switch from microservices to something they called “macroservices” due to the granular nature of microservices.


Where the Testing Pyramid lacks when it comes to testing microservices?

Initially, the testing pyramid worked well for testing monolith architecture. But when microservices came into picture, unit tests and E2E tests simply failed. Some major issues with the Martin Fowler’s pyramid are:


  • Testing for System-Level Confidence: Component level tests are comfortable and fast to implement. But the confidence slowly fades away when the number of services, endpoints, and integrations keeps on growing when new features are implemented. While it's important to test individual components, the real test comes when everything is integrated.


  • Hurdles with High-level testing: End-to-end tests are slow, prone to errors, hard to debug, and non-deterministic in general. Sometimes teams even avoid running them because of the high effort. They fail to data errors, and no one wants to spend time debugging them. It is not rewarding to debug and fix errors that are only related to the testing environment and not to the actual features of the system.


And that’s where devs and QA folks started exploring other approaches for microservices testing. This is where contract testing came forward as a tailored solution for microservices. It offers a simpler and more manageable way to ensure these services talks and performs as decided.


What is Contract Testing?

Contract testing is a way to test integrations between services and ensure that all the integrations are still working after new changes have been introduced to the system. This allows for faster, more focused testing since only the interactions are tested.


The main idea is that when an application or a service (consumer) consumes an API provided by another service (provider), a contract is formed between them. 

Contract testing breaks the testing boundaries between the services when compared to the component tests. This means that the services are no longer fully isolated from each other. The services are not directly connected either, like it happens with end-to-end tests. Instead, they are indirectly connected, and they communicate with each other using the contracts as a tool.

Interested in learning
how HyperTest can help you?

Get a Personalised Demo within 24 Hrs.

How Does Contract Testing Works?

Contract testing involves establishing and validating an agreement between two parties: the "Provider" (service owner) and the "Consumer" (service user).


How Does Contract Testing Works?

There are two main approaches to contract testing: consumer-driven and provider-driven.


👉Consumer-driven Contract Testing:

Consumer-driven contract testing is an approach where the consumer of a service takes the lead in defining the expected behavior of the service they depend on. They specify the contracts (expectations) that the service provider should meet, and then run tests to ensure that the provider complies with these contracts.


This approach puts consumers in control of their service dependencies and helps prevent unexpected changes or regressions in the service they rely on.


👉Provider-driven Contract Testing:

Provider-driven contract testing is initiated by the service provider. In this approach, the provider defines the contracts that consumers should adhere to when interacting with the service. The provider sets the expectations and provides these contracts to consumers.


Consumers, in turn, run tests to ensure that their usage of the service complies with the contracts specified by the provider. This approach allows the service provider to have a more proactive role in maintaining the integrity and stability of the service.


Provider-driven Contract Testing

👉Working of Consumer-driven Contract Testing:

  • The contract contains information about how the consumer calls the provider and what is being used from the responses.


  • As long as both of the parties obey the contract, they can both use it as a basis to verify their sides of the integration. The consumer can use it to mock the provider in its tests.


  • The provider, on the other hand, can use it to replay the consumer requests against its API. This way the provider can verify that the generated responses match the expectations set by the consumer.


  • With consumer-driven contracts, the provider is always aware of all of its consumers. This comes as a side product when all the consumers deliver their contracts to the provider instead of consumers accepting the contracts offered by the provider.


Benefits of Contract Testing

  • Separate Testing Pipelines: Both the Consumer and Provider carry out their testing procedures in isolation, without direct interaction with each other. This approach allows for asynchronous and independent testing workflows.


  • Maintenance: They are easier to maintain as you don't need to have an in-depth understanding of the entire ecosystem. You can write and manage tests for specific components without being overwhelmed by the complexity of the entire system.


  • Scalability: Contract tests scale efficiently because each component can be independently tested. This means that as your system grows, your build pipelines don't become increasingly time-consuming or complex.


  • Debugging and Issue Resolution: Contract tests simplify the debugging process. When a problem arises, you can pinpoint it to the specific component being tested. This means you'll often receive precise information like a line number or a particular API endpoint that is failing, making issue resolution more straightforward.


  • Speed: Contract tests are fast because they focus on individual components and don't require communication with multiple systems. This efficiency in testing helps save time and resources.


  • Repetition: Contract tests are repeatable, providing consistent and reliable results with each test run. This reliability is crucial in ensuring the stability of your software.


  • Local Bug Discovery: Contract tests are excellent at uncovering bugs on developer machines during the development process. This early detection helps developers catch and fix issues before pushing their code, contributing to better code quality and reducing the chances of defects reaching production.


  • Integration Failures: If discrepancies arise during either phase of Contract testing, it signals a need for joint problem-solving between the Consumer and Provider.


Use-cases of Contract Testing

Contract testing is a useful way to make sure microservices and APIs work well together. But it's not the best choice for all testing situations. Here are some cases where contract testing is a good idea:


Microservices Communication Testing:

  • Use Case: In a microservices architecture, different services need to communicate with each other. Contract testing ensures that these services understand and meet each other's expectations.


# Consumer Service Contract Test
def test_consumer_service_contract():
    contract = {
        "request": {"path": "/user/123", "method": "GET"},
        "response": {"status": 200, "body": {"id": 123, "name": "John"}}
    }
    response = make_request_to_provider_service(contract)
    assert response.status_code == 200

# Provider Service Contract Implementation
def make_request_to_provider_service(contract):
    # Code to handle the request and provide the expected response
    pass

API Integration/ Third-Party Integration Testing:

  • Use Case: When integrating with external APIs, contract testing helps ensure that the application interacts correctly with all the third-party APIs, preventing any compatibility issues and ensure that the code is reliable and secure.


  • Code Example:

# Contract Test for External API Integration
def test_external_api_contract():
    contract = {
        "request": {"path": "/products/123", "method": "GET"},
        "response": {"status": 200, "body": {"id": 123, "name": "Product A"}}
    }
    response = make_request_to_external_api(contract)
    assert response.status_code == 200

# Code for Making Requests to External API
def make_request_to_external_api(contract):
    # Code to send the request to the external API and handle the response
    pass

UI Component Interaction Testing:

  • Use Case: Contract testing can be applied to UI components, ensuring that interactions between different parts of a web application, like front-end and back-end, work as expected.


  • Code Example:

// Front-End UI Component Contract Test
it('should display user data when provided', () => {
    const userData = { id: 123, name: 'Alice' };
    const component = render(<UserProfile data={userData} />);
    const userElement = screen.getByText(/Alice/);
    expect(userElement).toBeInTheDocument();
});

// Back-End API Contract Implementation
app.get('/api/user/123', (req, res) => {
    res.status(200).json({ id: 123, name: 'Alice' });
});

Incorporating contract testing into development and testing workflows offers a proactive and effective approach to ensure the reliability and integrity of your systems. By verifying and documenting the expected behavior of different components and services, devs can minimize integration issues, catch bugs early in the development process, and foster smoother communication between microservices, APIs, and UI components.


Difference Between Contract Testing and

Integration Testing


Contract testing and integration testing are sometimes mistaken for one another, despite sharing a common end goal, they diverge significantly in their approaches.

Without contract testing, the only way to ensure that applications will work correctly together is by using expensive and brittle integration tests. Pact.io introduction


Contract testing

Integration testing

Scope

Focuses on verifying interactions at the boundary of an external service. It ensures that a service's output conforms to certain expectations and that it correctly handles input from another service. The primary concern is the "contract" or agreement between services.

Addresses the interaction between integrated components or systems as a whole. It checks if different components of an application work together as expected.

Test Depth

Is concerned with the correctness of interactions, not the internal workings of each service. It verifies if a service lives up to its "contract" or promised behavior.

Goes beyond just interactions. It can dive deep into the integrated system's logic, ensuring not only correct data exchange but also correct processing.

Test Maintenance

Contracts can be stable and act as a form of documentation. If teams adhere to the contract, changes in implementation shouldn't necessitate changes in the test.

Tends to be more brittle. A change in one service can often break integration tests, especially if they are end-to-end in nature.

Isolation

Uses mocked providers, allowing for testing in isolation. A consumer can be tested against a contract without having a running instance of the provider and vice versa.

Requires a more integrated environment. To test the interaction between two services, both services usually need to be up and running.

Feedback Loop

Provides quicker feedback since it tests against contracts and doesn't require setting up the entire integrated environment.

Might have a slower feedback loop, especially for end-to-end tests, because they can be more extensive and require more setup.

Purpose

Aims to give confidence that services fulfill their promises to their consumers. It's particularly useful in a microservices environment where teams develop services independently.

Ensures that when different components come together, they operate harmoniously. It's crucial for catching problems that might not show up in unit or contract tests.


Contract testing is about adhering to agreements between services, while integration testing is about achieving seamless operation when components are combined. Both types of testing are complementary and important in their own right.


Tools to Perform Contract Testing


Numerous tools exist in the market for conducting contract testing on APIs and microservices. Pact and Spring Cloud Contract are two of the most prominent tools in the realm of contract testing, widely recognized for their specific features and capabilities:


Let’s discuss the two of them:


Pact:


PACT stands out in the contract testing domain for its user-friendly interface and flexibility across various programming languages. Pact enables both consumers and providers to define and verify their contracts effectively.


It operates on a consumer-driven approach, meaning the consumer code generates the contract. This approach encourages consumer specifications to be fully considered in the contract, ensuring that the provided service meets these requirements.


pact tool

Pact’s versatility extends to support for numerous programming languages, including Ruby, JavaScript, and JVM languages, catering to a wide range of development environments.


Here’s a complete guide on how PACT contract testing works and where it still falls short.


Spring Cloud Contract:


This tool is specifically tailored for Java applications, particularly those developed using the Spring Boot framework.


Unlike Pact, Spring Cloud Contract adopts a provider-driven approach. It allows providers to define and verify their contracts, using Groovy or YAML for contract descriptions. This tool excels in automating the generation of test cases from these contracts, seamlessly integrating these tests into the build process.


This integration ensures that any changes in the service are immediately tested against the contract, thereby maintaining consistency and reliability in service interactions.


Both Pact and Spring Cloud Contract offer unique approaches to contract testing, catering to different methodologies and programming environments. The choice between them often depends on the specific needs of the project, the preferred development approach (consumer-driven vs. provider-driven), and the primary programming language used within the team.


We've covered PACT in detail here; head over now to get more insightful information here.

Frequently Asked Questions (FAQs)

1. What is contract testing in microservices?

In the realm of microservices, contract testing guarantees dependable communication among services. Each service establishes a set of contracts outlining anticipated interactions. Through testing, compliance with these contracts is verified, allowing for autonomous development and deployment while upholding system integrity.

2. What type of testing is a contract test?

Contract testing falls under the category of "consumer-driven contract testing," where service consumers specify expected interactions (contracts). Tests are then conducted by both service providers and consumers to ensure compliance with these contracts, promoting compatibility and reliability in microservices architecture.

3. What are the best practices for contract testing?

Best practices for contract testing in microservices include defining clear and concise contracts, ensuring comprehensive test coverage, automating tests for efficiency, maintaining version control for contracts, and integrating contract testing into the continuous integration pipeline. Collaborative communication between service providers and consumers, along with regular contract updates, promotes system reliability. Additionally, documenting contracts and test results facilitates understanding and troubleshooting.
bottom of page