Spiria logo.

“Resto”, a Python module to test the web back-end of a RESTful API

October 29, 2020.

I strongly believe that if something isn’t tested, then it probably won’t work. On the other hand, since writing tests can be tedious, any tool to help speed up and simplify testing is always welcome. In a recent project, I had to write the back-end of a web site, providing a RESTful API. I decided to use Python, as I had recent experience using it in a web back-end. I looked around for tools and best practices for testing a RESTful API, but didn’t get satisfying information. So, I decided to write my own little test helper, which I call resto.

One of the challenges in writing any tool is making it useful while keeping it simple. Since cumbersome tools go unused, I believe it’s better for a tool to be less than perfect but simple to use, and resto espouses this philosophy. As I initially described it, its design is centered around “JSON in/JSON out”, which grew to “headers in/headers out”.

Give resto the input data to your REST API, the expected output, and it will either figure out if it worked, or provide you with a minimum delta of the difference.

Example

Before going into the details of its use, here’s an example of it in action in an integration test. In this example, we call a /dogs end-point with a POST method to create a new dog.

From there, the call() function triggers the process of sending the request, comparing the results, and producing a dictionary of differences, which, if the test is successful, should be empty.

Python
def test_dogs_create_successful():
    """
    The goal of the test is to verify that the /dogs end-point
    can create a new dog when POST and return the correct info.
    """
    cfg = resto.Config()

    in_headers = prepare_login_for_write_tests(cfg)

    in_dog = {
        "first_name": "Wabby",
        "last_name": "Husky",
    }

    out_dog = {
        "dog": {
            "id": 4,
            "first_name": "Wabby",
            "last_name": "Husky",
        }
    }

    exp = resto.Expected(
        url = f’/dogs’,
        method = Method.POST,
        in_json = in_dog,
        in_headers = in_headers,
        out_json = out_dog,
        out_headers = { ‘Location’: f’{cfg.base_url}/dogs/4’ },
        status_code = 201
    )
    diff = exp.call(cfg)
    self.assertEqual({}, diff)

resto API

The resto API contains two classes and an enum. The “Config” class holds the configuration for resto, which, for now, only contains the base URL of the API to test. The “Method” enum selects which HTTP method to use: GET, POST, PUT or DELETE. The core of the resto module is in the “Expected” class. It calls a REST end-point and compares results.

The “Expected” class takes the description of the REST API to call and what to expect when it returns. None of the elements describing the REST API are required. resto takes advantages of Python’s named parameters to make them all optional. Following is a more specific description of the REST API and its expected result:

  • url: the URL to connect to.
  • method: the HTTP method to use.
  • params: URL parameters.
  • in_headers: the input HTTP headers.
  • out_headers: the expected output HTTP headers.
  • in_json: the input JSON.
  • out_json: the expected output JSON.
  • out_json_strict: choose to compare JSON exactly.
  • status_code: the expected status code returned.

The strict output JSON option controls the strictness of the comparison. If “True” (the default), then the output JSON must match exactly what was expected. Otherwise, if “False”, extra fields in the received JSON are ignored. This is useful to test just a subset of the result.

All in/out descriptions are in the form of Python dict. This allows the simple declaration of JSON and headers. Matching JSON and headers follows the usual comparison rules in Python, but with an extra twist to help compare text.

If the expected text starts with a tilde (“~”), then all that is necessary is to find the remaining text in the received text. For example, the expected text “~authorization” would match any text that contains “authorization”.

If the text is simply a star (“*”), then it matches any text. This is useful to verify receipt of a given JSON entry when its content is unknown. For example, the generated token text for authorization tokens cannot be known. Using “*” as the matching text would match any text token.

While the design may seem simple at first glance, it actually goes a little further in the background by trying to match the expected JSON or headers as best it can. This way, if an array is missing an element, it will try to match all other elements as best it can to return a minimal difference.

Once the description of the “Expected” REST call is created, the actual call to the REST API is done via the call() function of the object. It takes a single parameter, i.e. the configuration, for the base URL to be used to contact the REST API. The difference between expected output and actual output is returned as a Python dict. In other words, verifying success is as easy as a simple comparison with an empty dict, and printing the difference is as easy as printing that dictionary.

Availability

We used this simple module to write hundreds of integration tests for an internal project. The simplicity of the interface made writing tests a breeze, and was a major factor in our ability to test thoroughly. Using a combination of integration tests and unit tests, we achieved code coverage of almost 100% for almost all functionalities.

Our resto module can be found in this GitHub repo. The actual source code is in the file resto.py.

I hope you find it as useful as I did.