Published June 03, 2024. 4 min read
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.
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.
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.
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.
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.
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.
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
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.
2. On the backend, we have express server configuration having configured /orders API, which responds with the sample data as mentioned below
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
(We use chai to unit test in integration with our Pact tool)
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)
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.
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.