Initial setup

To get started, first we need a recent installation of the Python interpreter. Apart from Python, we also need to install a few libraries.

  • requests (REST API framework)
$ pip install -U requests
  • pytest (unit testing framework to provide us with a test runner, an assertion library and some basic reporting functionality)
$ pip install -U pytest pytest-html
  • jsonschema (json validator framework)
$ pip install -U jsonschema

With this, we are all set to write our first REST API test using Python.

Build first Python REST API test

Let's begin by writing a simple test that validates the response status code. All we need to do to get started is create a new Python file (we can create a new project in any IDE as well).

import requests

def test_get_employee_details_check_status_code_equals_200():
     response = requests.get("http://demo.example.com/employee/employee1")
     assert response.status_code == 200

What's happening here? We have imported the requests library to perform a REST API call. In the first line of the test, we call the get() method from the requests library to perform an HTTP GET call to the specified endpoint, and we store the entire response in a variable called response

We then extract the status_code property from the response object and write an assertion, using the pytest assert keyword to check that the status code is equal to 200, as expected. That's all there is to a first and admittedly very basic test against our API. 

Run with pytest

To run the test with pytest test framework, we need to define the function with a "test_" prefix and save the file with a "test_" prefix in the filename (e.g. test_post_headers_body_json.py). Let's run the test and see what happens. 

I prefer to do this from the command line because that's also how we will run the tests once they're part of an automated build pipeline. We can do so by calling pytest and telling it where to look for test files.

$pytest api_integration_test \01_basic_tests.py

Result:

============================= test session starts ==============================
platform darwin -- Python 3.9.0, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: /Users/xyz/Downloads/api_integration_test
plugins: metadata-1.10.0, html-3.0.0
collected 1 item                                                               

test_get_employee_details.py .                                           [100%]

============================== 1 passed in 0.45s ===============================

It looks like our test is passing. Since I never trust a test I haven't seen fail (and neither should you), let's change the expected HTTP status code from 200 to 201 and see what happens.

============================= test session starts ==============================
platform darwin -- Python 3.9.0, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: /Users/xyz/Downloads/api_integration_test
plugins: metadata-1.10.0, html-3.0.0
collected 1 item                                                                                                              

test_get_employee_details.py F                                                                                          [100%]

============================= FAILURES =============================
___________________________________ test_get_employee_details_check_status_code_equals_200 ____________________________________

    def test_get_employee_details_check_status_code_equals_200():
         response = requests.get("http://demo.example.com/employee/employee1")
>        assert response.status_code == 201
E        assert 200 == 201
E         +  where 200 = <Response [200]>.status_code

test_get_employee_details.py:5: AssertionError
============================= short test summary info =============================
FAILED test_get_employee_details.py::test_get_employee_details_check_status_code_equals_200 - assert 200 == 201
============================= 1 failed in 0.52s =============================

To generate an HTML report, run the test with the parameters below. 

$pytest -sv --html report.html
test report

Extending our test suite

Typically, we're interested in things other than the response HTTP status code too. For example, let's check if the value of the response content-type header correctly identifies that the response body is in JSON format and validates incoming JSON schema.

API endpoint: 

http://demo.example.com/employee_details?id=1

Sample response:

{
  "status": "ok",
  "employee": {
    "id": "1",
    "firstName": "Abc",
    "middleName": null,
    "lastName": "Xyz"
  }
}

For the above example, let's define JSON schema against which we will validate the incoming response.

Few imports:

import requests
import json
from jsonschema import validate
from jsonschema import Draft6Validator

JSON schema declaration:

schema = {
    "$schema": "https://json-schema.org/schema#",

    "type" : "object",
    "properties" : {
        "status" : {"type" : "string"},
        "employee": {
            "type": "object",
            "properties": {
                "id": { "type": "string" },
                "firstName": { "type": "string" },
                "middleName": {
                    "anyOf": [
                        {"type": "string"},
                        {"type": "null"}
                    ] },
                "lastName": { "type": "string" }
            },
            "required": ["id", "firstName", "lastName"]
        }
    }
}

Test case function:

def test_get_employees_validates_json_resonse_schema ():

    response = requests.get("http://demo.example.com/employee")
    
    # Validate response headers and body contents, e.g. status code.
    assert response.status_code == 200

    # Validate response content type header
    assert response.headers["Content-Type"] == "application/json"

    resp_body = response.json()

    # Validate will raise exception if given json is not
    # what is described in schema.
    validate(instance=resp_body, schema=schema)

Conclusion

We still have work to do to achieve more complicated unit test cases, but what we've discussed here is a good start.