Test des API externes avec des serveurs fictifs

Test des API externes avec des serveurs fictifs

En dépit d'être si utile,external APIs peut être difficile à tester. Lorsque vous atteignez une véritable API, vos tests sont à la merci du serveur externe, ce qui peut entraîner les problèmes suivants:

  • Le cycle de demande-réponse peut prendre plusieurs secondes. Cela peut ne pas sembler beaucoup au début, mais le temps s'accumule à chaque test. Imaginez appeler une API 10, 50, voire 100 fois lors du test de votre application entière.

  • L'API peut avoir des limites de taux définies.

  • Le serveur API peut être inaccessible. Peut-être que le serveur est en panne pour maintenance? Peut-être qu'il a échoué avec une erreur et que l'équipe de développement s'efforce de le rendre à nouveau fonctionnel> Voulez-vous vraiment que le succès de vos tests repose sur la santé d'un serveur que vous ne contrôlez pas?

Vos tests ne doivent pas évaluer si un serveur API est en cours d'exécution; ils devraient tester si votre code fonctionne comme prévu.

Dans lesprevious tutorial, nous avons introduit le concept d'objets fictifs, montré comment vous pouvez les utiliser pour tester du code qui interagit avec des API externes. This tutorial builds on the same topics, but here we walk you through how to actually build a mock server rather than mocking the APIs. Avec un serveur simulé en place, vous pouvez effectuer des tests de bout en bout. Vous pouvez utiliser votre application et obtenir des commentaires réels du faux serveur en temps réel.

Lorsque vous aurez terminé de travailler sur les exemples suivants, vous aurez programmé un serveur simulé de base et deux tests - un qui utilise le vrai serveur API et un qui utilise le serveur simulé. Les deux tests accéderont au même service, une API qui récupère une liste d'utilisateurs.

Note
Ce tutoriel utilise Python v3.5.1.

Commencer

Commencez par suivre la sectionFirst steps de l'article précédent. Ou récupérez le code desrepository. Assurez-vous que le test réussit avant de continuer:

$ nosetests --verbosity=2 project
test_todos.test_request_response ... ok

----------------------------------------------------------------------
Ran 1 test in 1.029s

OK

Test de l'API Mock

Une fois la configuration terminée, vous pouvez programmer votre faux serveur. Écrivez un test qui décrit le comportement:

project/tests/test_mock_server.py

# Third-party imports...
from nose.tools import assert_true
import requests


def test_request_response():
    url = 'http://localhost:{port}/users'.format(port=mock_server_port)

    # Send a request to the mock API server and store the response.
    response = requests.get(url)

    # Confirm that the request-response cycle completed successfully.
    assert_true(response.ok)

Notez qu'il commence à ressembler presque au vrai test API. L'URL a changé et pointe maintenant vers un point de terminaison d'API surlocalhost où le serveur fictif s'exécutera.

Voici comment créer un faux serveur en Python:

project/tests/test_mock_server.py

# Standard library imports...
from http.server import BaseHTTPRequestHandler, HTTPServer
import socket
from threading import Thread

# Third-party imports...
from nose.tools import assert_true
import requests


class MockServerRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        # Process an HTTP GET request and return a response with an HTTP 200 status.
        self.send_response(requests.codes.ok)
        self.end_headers()
        return


def get_free_port():
    s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM)
    s.bind(('localhost', 0))
    address, port = s.getsockname()
    s.close()
    return port


class TestMockServer(object):
    @classmethod
    def setup_class(cls):
        # Configure mock server.
        cls.mock_server_port = get_free_port()
        cls.mock_server = HTTPServer(('localhost', cls.mock_server_port), MockServerRequestHandler)

        # Start running mock server in a separate thread.
        # Daemon threads automatically shut down when the main process exits.
        cls.mock_server_thread = Thread(target=cls.mock_server.serve_forever)
        cls.mock_server_thread.setDaemon(True)
        cls.mock_server_thread.start()

    def test_request_response(self):
        url = 'http://localhost:{port}/users'.format(port=self.mock_server_port)

        # Send a request to the mock API server and store the response.
        response = requests.get(url)

        # Confirm that the request-response cycle completed successfully.
        print(response)
        assert_true(response.ok)

Commencez par créer une sous-classe deBaseHTTPRequestHandler. Cette classe capture la demande et construit la réponse à renvoyer. Remplacez la fonctiondo_GET() pour créer la réponse pour une requête HTTP GET. Dans ce cas, renvoyez simplement un état OK. Ensuite, écrivez une fonction pour obtenir un numéro de port disponible pour le faux serveur à utiliser.

Le bloc de code suivant configure en fait le serveur. Notez comment le code instancie une instance deHTTPServer et lui transmet un numéro de port et un gestionnaire. Next, create a thread, afin que le serveur puisse être exécuté de manière asynchrone et que votre thread principal de programme puisse communiquer avec lui. Faites du thread un démon, qui indique au thread de s'arrêter à la fin du programme principal. Enfin, démarrez le thread pour servir le serveur simulé pour toujours (jusqu'à la fin des tests).

Créez une classe de test et déplacez-y la fonction de test. Vous devez ajouter une méthode supplémentaire pour vous assurer que le faux serveur est lancé avant l'exécution des tests. Notez que ce nouveau code vit dans une fonction spéciale au niveau de la classe,setup_class().

Exécutez les tests et regardez-les passer:

$ nosetests --verbosity=2 project

Test d'un service qui rencontre l'API

Vous souhaitez probablement appeler plusieurs points de terminaison API dans votre code. Lorsque vous concevez votre application, vous allez probablement créer des fonctions de service pour envoyer des demandes à une API, puis traiter les réponses d'une manière ou d'une autre. Peut-être que vous stockerez les données de réponse dans une base de données. Ou vous transmettrez les données à une interface utilisateur.

Refactorisez votre code pour extraire l'URL de base de l'API codée en dur en une constante. Ajoutez cette variable à un fichierconstants.py:

project/constants.py

BASE_URL = 'http://jsonplaceholder.typicode.com'

Ensuite, encapsulez la logique pour récupérer les utilisateurs de l'API dans une fonction. Remarquez comment de nouvelles URL peuvent être créées en joignant un chemin d'URL à la base.

project/services.py

# Standard library imports...
from urllib.parse import urljoin

# Third-party imports...
import requests

# Local imports...
from project.constants import BASE_URL

USERS_URL = urljoin(BASE_URL, 'users')


def get_users():
    response = requests.get(USERS_URL)
    if response.ok:
        return response
    else:
        return None

Déplacez le code du faux serveur du fichier de fonctionnalités vers un nouveau fichier Python, afin qu'il puisse être facilement réutilisé. Ajoutez une logique conditionnelle au gestionnaire de demandes pour vérifier le point de terminaison API ciblé par la demande HTTP. Renforcez la réponse en ajoutant quelques informations d'en-tête simples et une charge utile de réponse de base. Le code de création et de lancement du serveur peut être encapsulé dans une méthode pratique,start_mock_server().

project/tests/mocks.py

# Standard library imports...
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
import re
import socket
from threading import Thread

# Third-party imports...
import requests


class MockServerRequestHandler(BaseHTTPRequestHandler):
    USERS_PATTERN = re.compile(r'/users')

    def do_GET(self):
        if re.search(self.USERS_PATTERN, self.path):
            # Add response status code.
            self.send_response(requests.codes.ok)

            # Add response headers.
            self.send_header('Content-Type', 'application/json; charset=utf-8')
            self.end_headers()

            # Add response content.
            response_content = json.dumps([])
            self.wfile.write(response_content.encode('utf-8'))
            return


def get_free_port():
    s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM)
    s.bind(('localhost', 0))
    address, port = s.getsockname()
    s.close()
    return port


def start_mock_server(port):
    mock_server = HTTPServer(('localhost', port), MockServerRequestHandler)
    mock_server_thread = Thread(target=mock_server.serve_forever)
    mock_server_thread.setDaemon(True)
    mock_server_thread.start()

Une fois vos modifications apportées à la logique, modifiez les tests pour utiliser la nouvelle fonction de service. Mettez à jour les tests pour vérifier l'augmentation des informations transmises par le serveur.

project/tests/test_real_server.py

# Third-party imports...
from nose.tools import assert_dict_contains_subset, assert_is_instance, assert_true

# Local imports...
from project.services import get_users


def test_request_response():
    response = get_users()

    assert_dict_contains_subset({'Content-Type': 'application/json; charset=utf-8'}, response.headers)
    assert_true(response.ok)
    assert_is_instance(response.json(), list)

project/tests/test_mock_server.py

# Third-party imports...
from unittest.mock import patch
from nose.tools import assert_dict_contains_subset, assert_list_equal, assert_true

# Local imports...
from project.services import get_users
from project.tests.mocks import get_free_port, start_mock_server


class TestMockServer(object):
    @classmethod
    def setup_class(cls):
        cls.mock_server_port = get_free_port()
        start_mock_server(cls.mock_server_port)

    def test_request_response(self):
        mock_users_url = 'http://localhost:{port}/users'.format(port=self.mock_server_port)

        # Patch USERS_URL so that the service uses the mock server URL instead of the real URL.
        with patch.dict('project.services.__dict__', {'USERS_URL': mock_users_url}):
            response = get_users()

        assert_dict_contains_subset({'Content-Type': 'application/json; charset=utf-8'}, response.headers)
        assert_true(response.ok)
        assert_list_equal(response.json(), [])

Remarquez une nouvelle technique utilisée dans le codetest_mock_server.py. La ligneresponse = get_users() est encapsulée avec une fonctionpatch.dict() de la bibliothèquemock.

Que fait cette déclaration?

N'oubliez pas que vous avez déplacé la fonctionrequests.get() de la logique de fonction vers la fonction de serviceget_users(). En interne,get_users() appellerequests.get() en utilisant la variableUSERS_URL. La fonctionpatch.dict() remplace temporairement la valeur de la variableUSERS_URL. En fait, il ne le fait que dans le cadre de l'instructionwith. Une fois ce code exécuté, la variableUSERS_URL est restaurée à sa valeur d'origine. Ce codepatchesest l'URL pour utiliser l'adresse du serveur fictif.

Exécutez les tests et regardez-les passer.

$ nosetests --verbosity=2
test_mock_server.TestMockServer.test_request_response ... 127.0.0.1 - - [05/Jul/2016 20:45:30] "GET /users HTTP/1.1" 200 -
ok
test_real_server.test_request_response ... ok
test_todos.test_request_response ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.871s

OK

Ignorer les tests qui atteignent la vraie API

Nous avons commencé ce tutoriel décrivant les avantages de tester un serveur factice au lieu d'un vrai, cependant, votre code teste actuellement les deux. Comment configurez-vous les tests pour ignorer le vrai serveur? La bibliothèque Python ‘unittest’ fournit plusieurs fonctions qui vous permettent de sauter des tests. Vous pouvez utiliser la fonction de saut conditionnel ‘skipIf’ avec une variable d’environnement pour activer et désactiver les tests réels du serveur. Dans l'exemple suivant, nous transmettons un nom de balise qui doit être ignoré:

$ export SKIP_TAGS=real

project/constants.py

# Standard-library imports...
import os


BASE_URL = 'http://jsonplaceholder.typicode.com'
SKIP_TAGS = os.getenv('SKIP_TAGS', '').split()

project/tests/test_real_server.py

# Standard library imports...
from unittest import skipIf

# Third-party imports...
from nose.tools import assert_dict_contains_subset, assert_is_instance, assert_true

# Local imports...
from project.constants import SKIP_TAGS
from project.services import get_users


@skipIf('real' in SKIP_TAGS, 'Skipping tests that hit the real API server.')
def test_request_response():
    response = get_users()

    assert_dict_contains_subset({'Content-Type': 'application/json; charset=utf-8'}, response.headers)
    assert_true(response.ok)
    assert_is_instance(response.json(), list)

Exécutez les tests et faites attention à la façon dont le test du serveur réel est ignoré:

$ nosetests --verbosity=2 project
test_mock_server.TestMockServer.test_request_response ... 127.0.0.1 - - [05/Jul/2016 20:52:18] "GET /users HTTP/1.1" 200 -
ok
test_real_server.test_request_response ... SKIP: Skipping tests that hit the real API server.
test_todos.test_request_response ... ok

----------------------------------------------------------------------
Ran 3 tests in 1.196s

OK (SKIP=1)

Prochaines étapes

Maintenant que vous avez créé un faux serveur pour tester vos appels d'API externes, vous pouvez appliquer ces connaissances à vos propres projets. Tirez parti des tests simples créés ici. Développez les fonctionnalités du gestionnaire pour imiter de plus près le comportement de la véritable API.

Essayez les exercices suivants pour passer au niveau supérieur:

  • Renvoie une réponse avec un état HTTP 404 (introuvable) si une demande est envoyée avec un chemin inconnu.

  • Renvoie une réponse avec un état HTTP 405 (méthode non autorisée) si une demande est envoyée avec une méthode non autorisée (POST, DELETE, UPDATE).

  • Renvoie les données utilisateur réelles pour une demande valide à/users.

  • Écrivez des tests pour capturer ces scénarios.

Récupérez le code desrepo.