Make it easier to test programs that work with APIs with vcrpy

Target readers

Article summary

Prerequisites

Introduction

Various APIs are published on the Web, and the number of services and programs that cooperate with them is increasing year by year. Along with that, I think that the opportunities to create programs and services that use such APIs are also increasing.

Problems to be dealt with this time

Sample program

Let's create a simple class that actually gets information from WebAPI and returns the extraction result.

sample_api_client.py


from http import client
import json


class SampleApiClient(object):

    def __init__(self, base_url):
        """
        An Api Client that accesses to resources under specific url.
        :param base_url: root url of API server that contains several resources (String)
        """
        self.base_url = base_url

    def get(self, resource, key='id', value=None):
        """
        An method to get entry of specific resources that satisfy following searching option.
        :param resource: a relative path from base_url that correspond to resource you want to access.(String)
        :param key: an attribute name of resource you want to filter by. default: id (String)
        :param value: a value of an attribute you want to filter by. (String)
        :return: filtered_data: a result of operation. (Array of Dictionary)
        """

        # create connection, and get raw data from API server
        conn = client.HTTPConnection(self.base_url, port=80)

        conn.request(method='GET', url=('/' + resource))
        response = conn.getresponse()

        raw_body = response.read()
        json_body = json.loads(raw_body.decode(encoding='utf-8'))

        # filter if value is specified.
        if value is not None:
            filtered_data = []
            for entry in json_body:
                if entry[key] == value:
                    filtered_data.append(entry)
        else:
            filtered_data = json_body

        return filtered_data


The above is a class to get information from the fake Web API below.

http://jsonplaceholder.typicode.com/

This time, we will test get, the method of the above class. This is a method to extract elements that match specific conditions from the above API.

Sample program test code

Define the test code to test this method as follows: In this test, we are testing whether the acquisition result of resource'todos' can be narrowed down by a specific title. (Target resource): http://jsonplaceholder.typicode.com/todos/

test_sample_api_client.py


from unittest import TestCase
from bin.sample_api_client import SampleApiClient
# Test Case Definition Starts from here.


class TestSampleApiClient(TestCase):

        def test_get_todo_by_title(self):

            client = SampleApiClient(base_url='jsonplaceholder.typicode.com')
            # a free online REST service that produces some fake JSON data.

            result = client.get(resource='todos', key='title', value="delectus aut autem")

            expected = [
                {
                "userId": 1,
                "id": 1,
                "title": "delectus aut autem",
                "completed": False
              }
            ]

            self.assertEqual(expected, result)

I will actually run it.

command
python -m unittest test_sample_api_client.py
Execution result
----------------------------------------------------------------------
Ran 1 test in 0.311s

OK

In this way, the test passed successfully. On the other hand, changes in the state of the API server can cause the test to fail. For example, if the communication path to the API server fails, the test execution result will be the following error.

======================================================================
ERROR: test_get_todo_by_title (tests.test_sample_api_client.TestSampleApiClient)
----------------------------------------------------------------------
Traceback (most recent call last):
 ~abridgement~
OSError: [WinError 10065]An attempt was made to perform a socket operation on an unreachable host.

----------------------------------------------------------------------
Ran 1 test in 21.026s

FAILED (errors=1)

problem

A unit test confirms the validity of a program as a single unit, but such a test implementation is affected by the connection destination (API server) and communication path. Besides, if another user posts a todo with the same title, the number of hits will be 2, and the test will fail even though the implementation itself is normal.

A common way to get around this problem is to create a mock, replace the submodule with the mock, and always fix the return value. (Details are omitted in this article) However, when creating a mock, it is necessary to define and manage the return value of the lower module for each API or resource to be linked, and it becomes a difficult task as the number of links increases.

Solution

Although the introduction has become long, I will introduce a library called "vcrpy" as a means to solve this problem. By using this library, HTTP requests / responses made to the API server can be recorded in a file and played back, saving the trouble of creating a mock.

Test code with vcrpy

First, install the vcrpy module in your environment with the following command.

pip install vcrpy

Then rewrite the test code to use vcrpy.

test_sample_api_client_with_vcr.py


from unittest import TestCase
import vcr
from bin.sample_api_client import SampleApiClient

# Instantiate VCR in order to Use VCR in test scenario.

vcr_instance = vcr.VCR(  # Following option is often used.
    cassette_library_dir='vcr/cassettes/',  # A Location to storing VCR Cassettes
    decode_compressed_response=True,  # Store VCR content (HTTP Requests / Responses) as a Plain text.
    serializer='json',  # Store VCR Record as a JSON Data
)

# Test Case Definition Starts from here.


class TestSampleApiClient(TestCase):
        @vcr_instance.use_cassette
        def test_get_todo_by_title(self):

            client = SampleApiClient(base_url='jsonplaceholder.typicode.com')
            # a free online REST service that produces some fake JSON data.

            result = client.get(resource='todos', key='title', value="delectus aut autem")

            expected = [
                {
                "userId": 1,
                "id": 1,
                "title": "delectus aut autem",
                "completed": False
              }
            ]

            self.assertEqual(expected, result)

There are two major changes:

Only this. If you run the test in this state, the HTTP request / response contents will be recorded as a file in the directory specified in the VCR option on the 8th line.

If you run this test again, there will be no communication to the API server and instead the HTTP response will be read from this file. In fact, try disconnecting the network again and running the test again.

Execution result
----------------------------------------------------------------------
Ran 1 test in 0.012s

OK

Summary

In this way, by using vcrpy, it is possible to reduce the influence of the state of the opposite device (API server) in the unit test. It's easy to implement, so please give it a try.

Recommended Posts

Make it easier to test programs that work with APIs with vcrpy
One liner that formats JSON to make it easier to see
Expand devicetree source include to make it easier to read
A module that makes it easier to write Perl-like filter programs in Python fileinput
When I try to push with heroku, it doesn't work
You who color the log to make it easier to see
Easy to make with syntax
Join csv normalized by Python pandas to make it easier to check
Make it possible to output a log to a file with go echo
I wrote Django commands to make it easier to debug Celery tasks
I tried to make a calculator with Tkinter so I will write it
[Zaif] I tried to make it easy to trade virtual currencies with Python