Writing Contract test for API endpoint using Pact

This post shows you a short example of how to use the Python module Pact to write Consumer Contract tests against Provider Contract. We will use Trello API endpoints for writing sample test. Note that this blog will only cover one sample test. Testers can write many more for a different response such as unauthorized access, server not found.


What is Contract testing?

Contract testing is a testing technique used for testing endpoints, for applications which are isolated. Messages sent or received confirms that they are adhering to a written Pact or Contract. This helps the tester to confirm all calls made to the Mock Provider, returns the same response as actual service. This reference link will give more information about Contract testing.


Background

Python package of Pact helps in writing consumer-driven python tests. As shown in the below diagram, while running Pact Test, Consumer does not contact actual Provider, but it will contact Mock Provider. The mock provider will validate the defined contract and send the appropriate response. The advantage over here is you don’t have to spin up actual Provider setup.



API Endpoint under Test

We will be using /1/boards/{id} endpoint for GET request of Trello API’s. This request will be used to fetch a single Trello board based on the ID. More information can be found at Trello API Documentation


Using pact-python

The Python module pact makes it really easy for us to define pact between REST API endpoint Provider and Consumer of those endpoints. The next steps will help to write the first test:

1.Define the Consumer and Provider objects that describe API endpoint and expected payload
2.Define the setup criteria for the Provider
3.Define the Consumer request using Pact
4.Define how the provider is expected to respond using Pact
5.Assert the response to validate the contract


1. Define the Consumer and Provider objects that describe API endpoint and expected payload

Create a new file called test_trello_get_board.py and add the following lines. Note that you will require to import Unittest, atexit and Pytest to run the test.

"contract test for Trello board"
import os
import json
import atexit
import unittest
import pytest
import requests
from pact import Consumer, Provider
 
# Setting up pact
pact = Consumer('Consumer').has_pact_with(Provider('Provider'))
pact.start_service()
atexit.register(pact.stop_service)
 
#setting up path for pact file
CURR_FILE_PATH = os.path.dirname(os.path.abspath(__file__))
PACT_DIR = os.path.join(CURR_FILE_PATH, '')
PACT_FILE = os.path.join(PACT_DIR, 'pact.json')
 
# Defining Class
class GetBoard(unittest.TestCase):
  def test_get_board(self):
   # Defining test method
     with open(os.path.join(PACT_DIR, PACT_FILE), 'rb') as pact_file:
        pact_file_json = json.load(pact_file)
        expected = pact_file_json
2. Define the setup criteria for the Provider

Using .given, setup criteria for the Provider will be defined as shown below.

(pact
  .given('Response Payload will be received as expected')
  .upon_receiving('a request for get trello board')
3. Define the Consumer request using Pact

Using .with_request, you can define the request type and API endpoint to which the request needs to be made.

(pact
  .given('Request for get trello board')
  .upon_receiving('a request for get trello board')
  .with_request('GET','/1/boards/1000')
4. Define how the Provider is expected to response using Pact

You can define the Provider response using .will_respond_with which will include status code and expected contract payload.

(pact
  .given('Request for get trello board')
  .upon_receiving('a request for get trello board')
  .with_request('GET','/1/boards/1000')
  .will_respond_with(200, body=expected))
5. Assert the response with expected contract

The below steps will help you to assert the response with a defined expected contract.

with pact:
    result = requests.get(pact.uri + '/1/boards/1000')
 
    self.assertEqual(result.json(), expected)
    pact.verify()

Putting it all together

Here is how our test looks:

"contract test for trello board"
import os
import json
import logging
import atexit
import unittest
import pytest
import requests
from pact import Consumer, Provider, Format
 
# Declaring logger
log = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
print(Format().__dict__)
 
# Setting up pact
pact = Consumer('Consumer').has_pact_with(Provider('Provider'))
pact.start_service()
atexit.register(pact.stop_service)
 
#setting up path for pact file
CURR_FILE_PATH = os.path.dirname(os.path.abspath(__file__))
PACT_DIR = os.path.join(CURR_FILE_PATH, '')
PACT_FILE = os.path.join(PACT_DIR, 'pact.json')
 
# Defining Class
class GetBoard(unittest.TestCase):
  def test_get_board(self):
     """
     Defining test method
     """
     with open(os.path.join(PACT_DIR, PACT_FILE), 'rb') as pact_file:
        pact_file_json = json.load(pact_file)
        expected = pact_file_json
 
        (pact
         .given('Request to send message')
         .upon_receiving('a request for response for send message')
         .with_request('GET','/1/boards/1000')
         .will_respond_with(200, body=expected))
 
        with pact:
           result = requests.get(pact.uri + '/1/boards/1000')
 
        self.assertEqual(result.json(), expected)
        pact.verify()

A sample pact.json file would look like below.

# pact.json
{
    "id":"1000",
 "desc":"Track changes to Trello's Platform on this board.",
 "descData":"test",
 "closed":"false",
 "idMemberCreator":"1000",
 "idOrganization":"1000",
 "pinned":"false",
 "url":"https://trello.com/b/dQHqCohZ/trello-platform-changelog",
 "shortUrl":"https://trello.com/b/dQHqCohZ",
 "prefs":{
    "permissionLevel":"org",
    "hideVotes":"true",
    "voting":"disabled",
    "comments":"test",
    "selfJoin":"true",
    "cardCovers":"true",
    "isTemplate":"true",
    "cardAging":"pirate",
    "calendarFeedEnabled":"true",
    "background":"1000",
    "backgroundImage":"test",
    "backgroundImageScaled":"test"
 },
 "labelNames":{
    "green":"Addition",
    "yellow":"Update",
    "orange":"Deprecation",
    "red":"Deletion",
    "purple":"Power-Ups",
    "blue":"News",
    "sky":"Announcement",
    "lime":"Delight",
    "pink":"REST API",
    "black":"Capabilties"
 },
 "limits":{
    "attachments":{
       "perBoard":{
          "status":"ok",
          "disableAt":36000,
          "warnAt":32400
       }
    }
 },
 "starred":"true",
 "memberships":"test",
 "shortLink":"test",
 "subscribed":"true",
 "powerUps":"test",
 "dateLastActivity":"test",
 "dateLastView":"test",
 "idTags":"test",
 "datePluginDisable":"test",
 "creationMethod":"test",
 "ixUpdate":2154,
 "templateGallery":"test",
 "enterpriseOwned":"true"
  }

After putting up everything you can run the test using python -m pytest test_trello_get_board.py.

Note: I have run the test on WSL(Windows Subsystem for Linux).

After the test runs successfully, you will find consumer-provider.json created. Here, you can verify response status as 200.The consumer-provide.json will look like as below:

# consumer-provide.json
{
  "consumer": {
    "name": "Consumer"
  },
  "provider": {
    "name": "Provider"
  },
  "interactions": [
    {
      "description": "a request for get trello board",
      "providerState": "Response Payload will be received as expected",
      "request": {
        "method": "GET",
        "path": "/1/boards/1000"
      },
      "response": {
        "status": 200,
        "headers": {
        },
        "body": {
          "id": "1000",
          "desc": "Track changes to Trello's Platform on this board.",
          "descData": "test",
          "closed": "false",
          "idMemberCreator": "1000",
          "idOrganization": "1000",
          "pinned": "false",
          "url": "https://trello.com/b/dQHqCohZ/trello-platform-changelog",
          "shortUrl": "https://trello.com/b/dQHqCohZ",
          "prefs": {
            "permissionLevel": "org",
            "hideVotes": "true",
            "voting": "disabled",
            "comments": "test",
            "selfJoin": "true",
            "cardCovers": "true",
            "isTemplate": "true",
            "cardAging": "pirate",
            "calendarFeedEnabled": "true",
            "background": "1000",
            "backgroundImage": "test",
            "backgroundImageScaled": "test"
          },
          "labelNames": {
            "green": "Addition",
            "yellow": "Update",
            "orange": "Deprecation",
            "red": "Deletion",
            "purple": "Power-Ups",
            "blue": "News",
            "sky": "Announcement",
            "lime": "Delight",
            "pink": "REST API",
            "black": "Capabilties"
          },
          "limits": {
            "attachments": {
              "perBoard": {
                "status": "ok",
                "disableAt": 36000,
                "warnAt": 32400
              }
            }
          },
          "starred": "true",
          "memberships": "test",
          "shortLink": "test",
          "subscribed": "true",
          "powerUps": "test",
          "dateLastActivity": "test",
          "dateLastView": "test",
          "idTags": "test",
          "datePluginDisable": "test",
          "creationMethod": "test",
          "ixUpdate": 2154,
          "templateGallery": "test",
          "enterpriseOwned": "true"
        }
      }
    }
  ],
  "metadata": {
    "pactSpecification": {
      "version": "2.0.0"
    }
  }
}

If you are a tester working on testing microservices then the above example will help you to add contract tests to your test strategy.


Leave a Reply

Your email address will not be published.