[PYTHON] Facilitez le test des programmes qui fonctionnent avec les API avec vcrpy

Lecteurs cibles

Résumé de l'article

Conditions préalables

introduction

Diverses API sont publiées sur le Web et le nombre de services et de programmes qui leur sont liés augmente d'année en année. Parallèlement à cela, je pense que les opportunités de créer des programmes et des services utilisant de telles API augmentent également.

Problèmes à résoudre cette fois

Exemple de programme

Créons une classe simple qui obtient réellement des informations de l'API Web et renvoie le résultat de l'extraction.

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


Ce qui précède est une classe qui obtient des informations de la fausse API Web ci-dessous.

http://jsonplaceholder.typicode.com/

Cette fois, nous allons tester get, la méthode de la classe ci-dessus. Il s'agit d'une méthode pour extraire des éléments qui correspondent à des conditions spécifiques de l'API ci-dessus.

Exemple de code de test de programme

Définissez le code de test pour tester cette méthode comme suit: Dans ce test, nous testons si le résultat de l'acquisition de "resource'todos" peut être réduit par un titre spécifique. (Ressource cible): 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)

Je vais vraiment l'exécuter.

commander
python -m unittest test_sample_api_client.py
Résultat d'exécution
----------------------------------------------------------------------
Ran 1 test in 0.311s

OK

De cette façon, le test a réussi. En revanche, si l'état du serveur API change, le test peut échouer. Par exemple, si le chemin de communication vers le serveur API échoue, le résultat de l'exécution du test sera l'erreur suivante.

======================================================================
ERROR: test_get_todo_by_title (tests.test_sample_api_client.TestSampleApiClient)
----------------------------------------------------------------------
Traceback (most recent call last):
 ~réduction~
OSError: [WinError 10065]Une tentative a été effectuée pour effectuer une opération de socket sur un hôte inaccessible.

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

FAILED (errors=1)

problème

Le test unitaire confirme la validité du programme en tant qu'unité unique, mais une telle implémentation de test est affectée par la destination de connexion (serveur API) et le chemin de communication. De plus, si un autre utilisateur publie un todo avec le même titre, le nombre de hits sera de 2 et le test échouera même si l'implémentation elle-même est normale.

Une manière courante de contourner ce problème consiste à créer une maquette, à remplacer les modules sous-jacents par la maquette et à toujours avoir une valeur de retour fixe. (Les détails sont omis dans cet article) Cependant, lors de la création d'une maquette, il est nécessaire de définir et de gérer la valeur de retour du module inférieur pour chaque API et ressource à lier, et cela devient une tâche difficile à mesure que le nombre de liens augmente.

Solution

Bien que l'introduction soit devenue longue, je présenterai une bibliothèque appelée "vcrpy" comme moyen de résoudre ce problème. En utilisant cette bibliothèque, les requêtes / réponses HTTP adressées au serveur API peuvent être enregistrées dans un fichier et lues, évitant ainsi la création d'une maquette.

Tester le code à l'aide de vcrpy

Tout d'abord, installez le module vcrpy dans votre environnement avec la commande suivante.

pip install vcrpy

Puis réécrivez le code de test pour utiliser 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)

Il y a deux changements majeurs:

Seulement ça. Si vous exécutez le test dans cet état, le contenu de la requête / réponse HTTP sera enregistré sous forme de fichier dans le répertoire spécifié dans l'option VCR sur la 8ème ligne.

Si vous réexécutez ce test, il n'y aura pas de communication avec le serveur API et à la place, la réponse HTTP sera lue à partir de ce fichier. En fait, essayez à nouveau de déconnecter le réseau et de relancer le test.

Résultat d'exécution
----------------------------------------------------------------------
Ran 1 test in 0.012s

OK

Résumé

De cette manière, en utilisant vcrpy, il est possible de le rendre moins sensible à l'état de l'appareil opposé (serveur API) dans le test unitaire. Il est facile à mettre en œuvre, alors essayez-le.

Recommended Posts

Facilitez le test des programmes qui fonctionnent avec les API avec vcrpy
Une doublure qui formate JSON pour le rendre plus facile à voir
Développer la source devicetree pour faciliter la lecture
Fileinput, un module qui facilite l'écriture de programmes de filtrage de type Perl en Python
Quand j'essaye de pousser avec heroku, ça ne marche pas
Vous qui coloriez le journal pour le rendre plus facile à voir
Rendre avec la syntaxe facile
Rejoignez CSV normalisé par les pandas Python pour faciliter la vérification
J'ai écrit des commandes Django pour faciliter le débogage des tâches Celery
[Zaif] J'ai essayé de faciliter le commerce de devises virtuelles avec Python