A Comprehensive Guide to Contract Testing with PACT and JavaScript
Written on
Chapter 1: Introduction to Contract Testing
How can we verify that our Microservices communicate effectively? What measures can we take to identify integration failures before they escalate?
Contract Testing
Originally, I authored this article as a guide for conducting PACT tests in Go. Given the growing prominence of NodeJS in Microservices development, I felt compelled to adapt this guide for JavaScript.
One aspect of software development that excites me is writing tests—be it unit or integration tests. The thrill of crafting a test case that highlights a failing function is unmatched. It brings me immense satisfaction to uncover bugs at this stage, potentially preventing them from causing issues in testing or, worse, in production.
Sometimes, I find myself staying up late to write additional tests; it's almost a hobby for me. I even dedicated 30 minutes to writing unit tests on my wedding day—let's keep that between us!
However, I’ve often been troubled by integration challenges among multiple Microservices. How can I ensure that two Microservices, each at different versions, won’t encounter integration issues? How do I verify that a new version won’t disrupt the API interface for existing consumers?
This knowledge is vital before we initiate extensive end-to-end testing scenarios. Otherwise, we risk waiting an hour only to discover we’ve broken the JSON schema.
Then one day at work, I learned about the possibility of implementing Contract Testing. After exploring the initial article on it, I was astounded—it felt revolutionary.
Contract Testing allows us to ensure that two parties can effectively communicate by testing them in isolation, verifying that both sides can handle the messages exchanged. The Consumer captures its communication with the Provider and establishes a Contract, which outlines the expected requests from the Consumer and the corresponding responses from the Provider.
Typically, the application code generates these Contracts automatically during the unit testing phase, ensuring that each Contract reflects the current state of the application.
Once the Consumer publishes the Contract, the Provider can utilize it. The Provider conducts Contract verification within its code (likely also during unit tests) and publishes the results.
In both phases of Contract Testing, we focus on one side without any direct interaction with the other party. This makes the entire process asynchronous and independent. If either phase fails, both parties must collaborate to resolve the integration issues, which may require the Consumer to adjust its integration code or the Provider to modify its API.
It’s essential to differentiate Contract Testing from Schema Testing and End-to-End Testing. Schema Testing is limited to one party and does not consider interactions with others, while Contract Testing verifies compatibility on both sides. End-to-End Testing evaluates a group of services running together, while Contract Testing assesses each service independently.
Now, let’s delve into what Contract Testing entails.
Chapter 2: Understanding PACT
PACT is a tool designed for Contract Testing. It facilitates the verification of communication between Consumers and Providers over HTTP, and it also accommodates message queue testing with systems like SQS, RabbitMQ, and Kafka.
To create Contracts, we utilize PACT DSL tailored for the specific programming language on the Consumer side. The Contract encompasses interactions that specify expected requests and minimal responses.
During test execution, the Consumer sends requests to a Mock Provider that uses the defined interactions to validate whether actual HTTP requests align with expected ones. If they match, the Mock Provider returns the anticipated minimal response, allowing the Consumer to verify its expectations.
On the Provider side, we use these Contracts to ascertain whether the server can meet the outlined expectations. Verification results can then be published to track compatibility between Consumer and Provider versions.
When conducting tests on the Provider side, a Mock Consumer sends the expected request to the Provider. The Provider checks if the HTTP request meets the expected format and returns a response. Ultimately, the Mock Consumer compares the actual response with the minimal expected response and provides the verification result.
All Contracts and Verification results can be stored on a PACT Broker, a tool that developers typically host and maintain independently on most projects. However, public options like Pactflow are also available.
The first video titled "Testing Talks Conference 2023 Melbourne | A Comprehensive Guide to Contract Testing with Pact" provides insights and detailed methodologies on Contract Testing with PACT.
Chapter 3: Implementing Simple Server and Client in NodeJS
To illustrate the creation of a Contract, let’s look at a simple server and client. The server will expose an endpoint /users/:userId to return user details by their ID. The complete server code resides in a single file named server.js. For this demonstration, I utilized the Express web framework for NodeJS, though any framework (or even a bare-bones setup) would suffice.
The client code is straightforward and is contained in client.js. It sends a GET request to the /users/:userId endpoint and returns the payload upon receiving the result. This relies on the Axios library for HTTP requests.
Both client and server code include dedicated functions for handling requests and responses, which is crucial for later testing.
The second video titled "Contract Testing using PACT with CODE Examples" elaborates on practical code examples for implementing Contract Testing with PACT.
Chapter 4: Writing PACT Tests for NodeJS
Creating PACT tests for NodeJS mirrors the process of writing unit tests. Here, we should use the NPM package from Pact Foundation and I recommend including Mocha and Chai.
In the example provided, I defined a PACT Provider that operates a Mock Server in the background. The key aspect of the test involves defining an Interaction, which encompasses the Provider's state, the test case name, the expected request, and the anticipated minimal response.
Numerous attributes can be defined for both requests and responses, including body, headers, query parameters, status codes, and more.
Once we establish the Interaction, verification is the next step. The PACT test executes the client, which now communicates with the PACT Mock Server instead of a real server. I ensured this by directing the getUserByID method to the Mock Server's host.
If the actual request aligns with the expected one, the Mock Server sends back the anticipated minimal response. Within the test, we perform a final check to confirm that our method returns the correct user data extracted from the JSON body.
The final step involves writing the Interaction as a Contract. By default, PACT stores the Contract in the pacts folder, although this can be modified during PACT Provider initialization.
After executing the code, the output should resemble the following:
[2022-04-23 20:15:16.158 +0000] INFO (92784 on TS-NB-089): [email protected]: Pact running on port 57645
[2022-04-23 20:15:16.468 +0000] INFO (92784 on TS-NB-089): [email protected]: Setting up Pact with Consumer "example-client" and Provider "example-server"
using mock service on Port: "57645"
✔ should get the right user data
1 passing (2s)
Chapter 5: Validating the Server with PACT Tests
Writing PACT tests for the server is a simpler task. Here, we validate the Contracts provided by the client. We also structure PACT tests for servers as unit tests.
The client has already established a Contract in JSON format that contains all interactions. Our job is to set up the PACT Verifier and execute the verification of the Provider against the specified Contract, provided as a parameter.
During verification, the PACT Mock Client sends anticipated requests to the server as specified in the Contract's Interactions. The server processes the request and returns an actual response. The Mock Client then matches this response with the expected minimal response.
If the verification process succeeds, the output should look like this:
Pact test for server
handleUser
[2022-04-23 20:25:49.993 +0000] INFO (92887 on TS-NB-089): [email protected]: Verifying provider
[2022-04-23 20:25:50.056 +0000] INFO (92887 on TS-NB-089): [email protected]: Verifying Pacts.
[2022-04-23 20:25:50.058 +0000] INFO (92887 on TS-NB-089): [email protected]: Verifying Pact Files
[2022-04-23 20:25:51.616 +0000] WARN (92887 on TS-NB-089): [email protected]: No state handler found for "User Alice exists", ignoring
[2022-04-23 20:25:51.709 +0000] INFO (92887 on TS-NB-089): [email protected]: Pact Verification succeeded.
✔ should get the right user data (1716ms)
1 passing (2s)
Chapter 6: Utilizing PACT Broker with Pactflow
As previously highlighted, leveraging PACT testing necessitates the use of a PACT Broker. It’s impractical for clients to access Contracts stored in physical files from the server pipelines.
Development teams should implement a dedicated PACT broker for their project. A Docker image from the PACT Foundation can be utilized as part of the infrastructure.
If you prefer a managed PACT broker solution, Pactflow is a great choice, and registration is straightforward. Throughout this article, I utilized the trial version of Pactflow, which allows storage of up to five Contracts.
To publish Contracts to Pactflow, minor adjustments are needed in the client.pact.test.js file. These adaptations include defining a PACT publisher that uploads Contracts post-test execution.
After registering with Pactflow, you will receive a new host for your PACT broker. Additionally, an API token is necessary to finalize the publisher’s definition in the code, which can be found in the dashboard’s settings.
Executing the modified client test will upload the first Contract to Pactflow, tagged with 1.0.0, latest, and master (the latter being added by default).
To differentiate the client, I modified the test to send requests to the endpoint /user/{userId} instead of /users/{userId}. I also updated the Tag and ConsumerVersion to 1.0.1 from 1.0.0. After execution, the additional Contract will appear.
Next, I’ll modify the server test. The only changes pertain to the verification process, which should now incorporate the PACT Broker host, API token, and the decision to publish verification results.
Additionally, it should include selectors for the Consumer name and version to accurately verify against the relevant Contract version. The first successful execution will check the client version 1.0.0.
The second execution, which is expected to fail, will check the client version 1.0.1. This failure is anticipated, as the server still listens to the /users/{userId} endpoint.
To resolve the integration issue between the latest client and server, we must update either component. I chose to adjust the server to accommodate the new /user/{userId} endpoint.
After updating the server to version 1.0.1 and re-running the PACT verification, the test passes again, and the new verification results are published on the PACT broker.
On Pactflow, similar to any other PACT broker, we can also review the verification history of each Contract by accessing a specific Contract from the dashboard overview and checking its Matrix tab.
Conclusion
Creating PACT tests is an efficient and economical method to streamline our pipeline. By validating Consumers and Providers early in our CI/CD process, we gain timely feedback regarding our integration outcomes.
Contract tests enable us to utilize the actual versions of our clients and servers, validating them in an isolated process to determine compatibility.
What has been your experience with Contract testing, and specifically with PACT?
More content at PlainEnglish.io.
Sign up for our free weekly newsletter. Follow us on Twitter, LinkedIn, YouTube, and Discord.