PACTify: Seamless contract testing for APIs

Published June 03, 2024. 4 min read

Team EnLume Author

Teja Gowtham, Software Engineer, EnLume

Have you ever experienced the frustration of a seemingly perfect application falling apart during integration? Imagine a front-end application flawlessly fetching data from an API, only to encounter errors when deployed because the API structure has changed. This is a common challenge in modern development, especially with microservices architectures. Fear not, developers! PACT offers an effective solution for ensuring smooth API integration and preventing these headaches.

Intro to PACT

Building modern applications often involves integrating various components, like front-end requests and API responses. Ensuring these integrations work seamlessly can be a challenge. Here's where PACT comes in. PACT is a powerful tool that utilizes contracts to streamline testing for these integrations, focusing specifically on HTTP interactions. PACT employs a consumer-driven approach, where the consumer defines the expected behavior (the contract), and the producer (often the backend API) verifies its adherence to that contract.

img

From the diagram, let us say multiple Consumers(Front-end Requests) are served by a single Provider(API), where each consumer consumes only the required data from the provider response. So, in the future, when a particular consumer requests the provider to change the API, the new provider may break the older consumers' requirements. The only way to ensure the whole application works is through complete integration tests and dedicated deployment environments, which are expensive. To address this problem, we have PACT, which is a consumer-driven contract testing tool.

What is contract testing?

Contract testing is a technique where the HTTP request and response are tested in isolation, conforming to a shared understanding provided in the contract. It helps in microservice architectures where multiple tiny services are integrated into a complete application. Having well-formed contract tests makes it easy for developers to avoid version hell, making it a great tool for microservice development and deployment.

PACT for contract testing

Using PACT, we write tests for the consumer. Tests are driven by the unit test framework inside the consumer codebase, and a contract, which we call Pact, is generated during consumer testing and contains the consumer expectations. The pact file is then shared with the provider(usually through a PACT broker), and the provider verifies the consumer’s expectations with the Pact file as a reference.

    img

    How Pact works

    Each pact is a collection of interactions to handle multiple paths. (eg. /orders, /users) where each interaction tells us.

    1. An expected request, telling what the consumer is supposed to send the provider.

    2. A minimal expected response, describing a response that the consumer wants from the provider.

    Consumer pact test on each interaction

    Assuming the provider returns the expected response for this request, all it asks is whether the consumer code correctly generates the request and handles the expected response.

    1. The diagram below shows that the pact framework instantiates a mock provider on a random port when executing consumer tests.

    2. An actual request is sent to the mock provider, defined in the consumer code.

    3. Upon receiving the request, the mock provider compares the actual request with the expected one in the interaction. If the comparison is successful, the mock provider emits the minimal expected response (provided in the interaction) to the consumer.

    4. Now, the consumer test code confirms the response and its handling.

    5. When the consumer test is successful, it generates a pact file.

    img

    As we now have the Pact file, we share the file with the provider, usually through a Pact broker or by any automated means.

    Provider pact test on each interaction

    Unlike consumer tests, provider verification is entirely driven by the pact framework

    1. In provider verification, a mock consumer is created, which refers to the interaction and sends the expected request.
    2. When the provider responds, the provider’s actual response is compared with the minimal expected response.
    3. The provider verification passes when the provider’s actual response contains at least the data described in the minimal expected response.

    img

    Example:

    In this, we shall go through a sample application and add tests thereafter:

    1. On the front end we have a client fetchOrders which fetches all the orders by making an API call. From the response, the outlook of each order is described in the Order class.

    img
    img
    img

    2. On the backend, we have express server configuration having configured /orders API, which responds with the sample data as mentioned below

    img

    Till now, this is our usual application without any tests. Now we need to integrate PACT on the consumer and provider side. So on the consumer side, we create consumer.spec.js and on the provider side, we have provider.spec.js.

    consumer.spec.js

    img

    (We use chai to unit test in integration with our Pact tool)

    img

    The above consumer tests on success generate a pact file, which shall be shared with the provider.

    provider.spec.js

    (We refer to the shared pact and verify if the provider meets all the consumer expectations)

    img

    Running the provider tests verifies if the provider's actual responses meet the minimum expectations outlined in the Pact file. These expectations are established by the consumer during testing. Any deviation from these expectations, like the missing "value" key in the first item of the "items" array in the example, will cause the provider tests to fail. This failure signals a potential incompatibility and prompts the provider to address the discrepancy before deployment.

    img

    Conclusion

    PACT offers a consumer-driven contract testing approach, enabling developers to write tests that focus on the expected behavior of APIs. This not only simplifies integration testing but also promotes a collaborative development environment where both consumers and providers can ensure compatibility and avoid version conflicts. By leveraging PACT, developers can build applications with confidence, knowing that different components will work together harmoniously.