Introduction to Bi-Directional Contract Testing

In our previous blog we gave you an introduction to contract testing, explained Consumer-Driven Contract Testing approach ,setting up Consumer-Driven Contract tests, using Provider states and some of its drawbacks as well. In this blog we will introduce you to the concept of Bi-Directional Contract Testing. We will also show you how to setup Bi-Directional contract tests with an illustrative example.

Note: As for early 2023, the technique of Bi-Directional contract testing is still a fairly new. Qxf2 currently lacks significant real-world experience in Bi-directional contract testing. We are sharing insights derived from our internal experiments, providing you with our firsthand learnings. While we are in the process of developing our understanding, our opinions on the subject are still evolving and not yet fully formed.

What is Bidirectional Contract Testing?

Bi-Directional contract testing is an advanced approach to contract testing that addresses some of the limitations of the Consumer-Driven Contract Testing (CDCT) methodology. While CDCT focuses on testing the contracts from the consumer’s perspective, bidirectional contract testing extends this concept by also verifying the contracts from the provider’s perspective. This ensures a more comprehensive and reliable testing process for microservices-based architectures. Bi-Directional Contract Testing uses a schema comparison approach to compare the Consumer contract against the Provider contract. As of now , Bidirectional contract tests is only supported by PactFlow.

PactFlow is an enhanced version of the Pact Broker. It offers additional functionalities necessary for effectively utilizing Pact in large-scale environments. Some of the features that PactFlow offers are single sign-on (SSO), user administration, an enhanced user interface (UI), secure storage for sensitive information and support for Bi-Directional Contract Testing.

How Bidirectional Contract Testing Works

The following image shows the workflow of Bi-Directional Contract Testing:

Pact Bi-Directional contract testing workflow
Workflow of Bi-Directional Contract Testing

1. Just like Consumer-Driven contract testing, the Consumer writes a unit test that runs against a mock Provider to generate a contract.
2. The mock Provider can be setup using Pact within the same unit test or setup independently using any other tool like WireMock.
3. The mock Provider is configured to return an expected response when it receives a specific request.
4. The test then sends this request to the mock provider. The mock Provider checks if the request matches the expected request and then returns the response that it was configured to return.
5. The test checks if the response received from the mock Provider matches with the expected response.
6. If this passes then the expected request and the expected response for that request is captured in a contract and uploaded to a PactFlow.
7. In the Provider end, we first need to generate a Provider contract, such as an OpenAPI documentation for the Provider API.
8. The Provider contract has to be validated by testing it against the Provider using functional API testing tools such as Postman, ReadyAPI etc. or even through code using tools like Swashbuckle or Schemathesis
9. The results of this test are stored into a file
10. The Provider contract is then uploaded to PactFlow along with the test result.
11. PactFlow then compares the Consumer contract with the Provider contract to ensure the Consumer contract is a valid subset of the Provider contract.
12. Finally, the verification results are generated indicating if either Consumer or Provider breaches each others contract or not
For a more detailed explanation on the workflow of Bi-Directional Contract Testing, we have created a video for the same:

Setting up Bidirectional Contract Test for Newsletter Automation Application

In our previous blog we had shown setting up Consumer-Driven Contract tests for the Qxf2 Newsletter Automation application. Now let’s see how we can setup Bi-Directional contract test for it.
Now, the first step is to create a Consumer contract. We already created this contract and went through the steps to do so in our previous blog. We will use the same Consumer contract for our Bi-Directional contract test as well. So, lets see how we can upload this Consumer contract to PactFlow.

Uploading Consumer Contract to PactFlow

Once you signup to PactFlow, you would be provided with you’re own PactFlow server. We need to upload our contracts to this server.
1. We will be using the Pact Broker Client standalone executable which we setup in our previous blog here as well, to upload our contracts.
2. We can upload our consumer contract with the following command:

<path-to-your-pact-standalone-folder>/bin/pact-broker.bat publish <Path-to-your-contract> --broker-base-url <Pact-Broker-URL> --consumer-app-version <Version> --branch <Branch-name> --broker-token <PactFlow-Read/write-API-token>

Example:

/d/pact/bin/pact-broker.bat publish /d/code/qxf2-lambdas/pacts/newsletterlambda-newsletterapi.json --broker-base-url https://test-qxf2.pactflow.io/ --consumer-app-version 1.0 --branch Newsletter_Consumer --broker-token dummytoken

Note: You can find your PactFlow Read/Write API token under setting->API Tokens in your PactFlow server UI.
3. If the contract get’s uploaded successfully, you should be able to see a similar output in your terminal:

Successful publication of Consumer contract to  PactFlow
Command-line output for Successful Publication of Consumer contract to PactFlow.

4. Login to your PactFlow server. You should be able to see a new Pact created with the name of the Consumer and Provider that you had specified in your Contract.
PactFlow UI showing the Consumer contract
Consumer contract as seen in PactFlow.

Now, that we have our Consumer contract uploaded, let’s see what goes on in the Provider end.

Getting Provider Contract

In the Provider end, we first need to get a Provider Contract. This contract is an OpenAPI documentation for our Provider API. If you don’t have it already , you can modify your Provider codebase using tools like APIFlask to generate an OpenAPI documentation. In our case we already have an OpenAPI documentation for the Newsletter Automation API provider, which will be our Provider contract. We have named this file as newsletter_automation_oas.json
1. Let’s see the specification for the API endpoint that we will be testing in the documentation

        "/api/articles": {
            "post": {
                "parameters": [],
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {}
                            }
                        },
                        "description": "Successful response"
                    }
                },
                "summary": "To add articles through api endpoints"
            }
        }

2. We need to modify the above specification by adding an expected request and also define the schema for the expected response. Tools like Schemathesis would use this API specification to generate test cases.

        "/api/articles": {
            "post": {
                "parameters": [],
                "requestBody": {
                    "content": {
                        "application/x-www-form-urlencoded": {
                            "schema": {
                                "additionalProperties": false,
                                "type": "object",
                                "properties": {
                                    "url": {
                                        "type": "string",
                                        "minLength": 15,
                                        "pattern": "\\Awww[.]contract-test-[0-9][.]com\\Z"
                                    },
                                    "category_id": {
                                        "type": "integer",
                                        "minimum":1,
                                        "maximum":5
                                    },
                                    "article_editor":{
                                        "type": "string",
                                        "minLength": 5,
                                        "pattern":"\\APact-tester\\Z"
                                    }
                                },
                                "required": [
                                    "article_editor",
                                    "category_id",
                                    "url"                                  
                                ]
                            }
                        }
                    },
                    "required": true
                },
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "additionalProperties": false,
                                    "type": "object",
                                    "properties": {
                                        "message": {
                                            "type": "string"
                                        }
                                    },
                                    "required": [
                                        "message"                                  
                                    ]
                                }
                            }
                        },
                        "description": "Successful response"
                    }
                },
                "summary": "To add articles through api endpoints"
            }
        }

3. As you can see in the above JSON file, we have added the expected request to the API specification using the requestBody block. We have defined the content-type and schema properties for the request body. Tools like Schemathesis would use these details to automatically generate different request bodies(test cases).
4. We have also defined the properties of the response body schema. The test would use this to compare if the actual response received from the provider by passing the expected request, matches with the expected response body that we have defined.
Now that we have defined our OpenAPI specification, let’s see how we can validate this specification against the Provider.

Verifying the Provider Contract Against the Provider

In order to validate the authenticity of our Provider Contract, we need to test this contract against the Provider.
1. In this case, we will be using Schemathesis to write a simple test to validate the documentation of the API endpoint that the Consumer uses.
2. We will be writing the test using Python. So, let’s start off by installing the required libraries.
pip install schemathesis hypothesis pytest
3. Next create a new Python file. Lets name it newsletter_automation_schema_test.py
4. Now, lets import the required libraries in our Python test

import os
import schemathesis
from hypothesis import settings

5. Next, lets pass the OpenAPI documentation and the URL of our Provider API to schemathesis.

schema = schemathesis.from_path("newsletter_automation_oas.json", base_url="http://127.0.0.1:5000")

6. Now, we are all set to write our test function to validate the openAPI documentation against our Provider API.

@settings(max_examples=20)
@schema.parametrize(endpoint="/api/articles\\Z")
def test_newsletter_api(case):
    """
    Test the Newsletter API for retrieving articles.
 
    Args:
        case: Test case generated by schemathesis.
    """
    case.call_and_validate(headers={"x-api-key": os.environ.get("API_KEY")})

In the above code we use @settings(max_examples=20) to automatically generate 20 random test cases based on the specification provided in the opneAPI documentation. These test cases are passed as an argument to the test function. schema.parametrize is used to set the API endpoint to run our test against. And finally, the call_and_validate method is used to send the auto-generated request to the Provider API and validate if the response received matches with the response specified in the OpenAPI documentation.
7. Our complete code should look similar to this:

"""
This module contains a test for the Newsletter Automation API.
 
The test validates the API's endpoint for retrieving articles ie. '/api/articles',
against the OpenAPI documentation.
 
It uses schemathesis and hypothesis libraries for test case generation and validation.
"""
 
import os
import schemathesis
from hypothesis import settings
 
schema = schemathesis.from_path("newsletter_automation_oas.json", base_url="http://127.0.0.1:5000")
 
@settings(max_examples=20)
@schema.parametrize(endpoint="/api/articles\\Z")
def test_newsletter_api(case):
    """
    Test the Newsletter API for retrieving articles.
 
    Args:
        case: Test case generated by schemathesis.
    """
    case.call_and_validate(headers={"x-api-key": os.environ.get("API_KEY")})

8. Run the test using command:

 pytest newsletter_automation_schema_test.py  --junitxml=newsletter_automation_api_test_result.xml

The above command runs the test and stores the test results in newsletter_automation_api_test_result.xml file.
Now that we have our test results ready, lets see how we can upload the Provider contract and test results to PactFlow.

Uploading Provider Contract to PactFlow

We will use the Pact Broker Client standalone executable, the same tool that we used to upload the Consumer contract, to upload the Provider Contract as well.
1. The following command is used to upload the Provider contract

<path-to-your-pact-standalone-folder>/pactflow.bat publish-provider-contract <Path-to-your-Provider-contract> --broker-base-url <Pact-Broker-URL> --broker-token <PactFlow-Read/write-API-token> --provider <Provider-name> --provider-app-version <Provider-version> --branch <Provider-branch-name> --content-type <content-type-of-Provider-contract> --verification-results <path-to-test-result-file> --verifier <tool-used-to-verify> --verification-results-content-type <content-type-of-test-results-file>  --verification-exit-code <status-of-test>

Example:

/d/pact/bin/pactflow.bat publish-provider-contract  newsletter_automation_oas.json --broker-base-url="https://test-qxf2.pactflow.io" --broker-token dummytoken --provider NewsletterAPI --provider-app-version 1.0 --branch Newsletter_Provider --content-type application/json --verification-results newsletter_automation_api_test_result.xml --verifier schemathesis --verification-results-content-type xml  --verification-exit-code=0

Note: Make sure the Provider name matches with the name of the Provider specified in the Consumer contract.
2. On successful publication of the contract, your terminal output would look similar to this:

Successful publication of Provider contract to PactFlow
Command-line output for Successful Publication of Provider contract to PactFlow.

3. Next, login back to your PactFlow server. You would notice that the Provider contract details would be displayed now alongside the Consumer contract
PactFlow UI showing the Provider contract
Provider contract as seen in PactFlow.

Viewing Test Results on PactFlow

PactFlow compares the Consumer contract with the Provider contract to check if the Consumer contract is a valid subset of the Provider contract. The verification results are then published.
1. You would be able to see if the verification was successful or not in the overview page itself.

Overview page in Pactflow displaying the contract verification result
Contract Verification result shown in PactFlow

2. In our case, the verification was successful. However, if the verification fails, you can click on View Contracts option to check the details of what went wrong. You would be redirected to a contract comparison page where you can see the details of the failures if any.
Contract comparison details for Bi-Directional contract testing  in PactFlow
Contract comparison details

3. Let’s take a look at the error we get when the response body in the Consumer contract does not match with the response specified in the Provider contract.
Pactflo's comparission of contracts having varying response bodies.
Contract comparison result when response bodies in the contracts are incompatible

4. To know the specific interaction that failed, click on the Consumer Contract tab.
Interactions from Consumer contract that failed
Status of each interaction in the consumer contract.

5. Similarly. lets take a look at the error we get when there is a mismatch in the response status code.
Contact comparision result for response status mismatch
Contract comparison result when there is a mismatch in response status.

6. And to know the interaction that failed, let’s go to the Consumer Contract tab.
Status of interactions in the Consumer contract
Status of each interaction in the consumer contract.

7. Finally, lets also take a look at the error response we get when there is a mismatch in response type.
Contact comparision result for response type mismatch
Contract comparison result when there is a mismatch in response type.

8. We have given you just a few examples of failures that result in contract incompatibility. There are numerous other instances of failures which causes incompatibilities between contracts.

Final thoughts

As seen in the above example , Bi-Directional Contract Testing is an excellent way to get started with contract testing. It leverages the existing tests and specifications, as a result enabling us to get our contracts uploaded and verified pretty easily. Bi-Directional contract Testing is still a fairly new concept as of now. As Pact expands its support for various tools and specification standards, the popularity of Bi-Directional Contract testing is expected to rise.

Qxf2’s Technical Testing Expertise

Qxf2 brings you the technical testing expertise your company needs. Our team of experienced testers possesses in-depth knowledge of testing methodologies and cutting-edge technology. We seamlessly integrate with small engineering teams, collaborating closely to deliver high-quality results. With a flexible approach, we adapt to various tech stacks, ensuring our testing capabilities align with your unique requirements. Trust Qxf2 to empower your software testing initiatives and drive success. Contact us today!


Leave a Reply

Your email address will not be published. Required fields are marked *