Testen externer APIs mit Mock-Servern

Testen externer APIs mit Mock-Servern

Obwohlexternal APIs so nützlich sind, kann es schwierig sein, sie zu testen. Wenn Sie eine tatsächliche API treffen, sind Ihre Tests dem externen Server ausgeliefert, was zu folgenden Schwachstellen führen kann:

  • Der Anforderungs- / Antwortzyklus kann einige Sekunden dauern. Das scheint auf den ersten Blick nicht viel zu sein, aber die Zeit hängt mit jedem Test zusammen. Stellen Sie sich vor, Sie rufen eine API 10, 50 oder sogar 100 Mal auf, wenn Sie Ihre gesamte Anwendung testen.

  • Für die API sind möglicherweise Ratenlimits eingerichtet.

  • Der API-Server ist möglicherweise nicht erreichbar. Möglicherweise ist der Server wegen Wartungsarbeiten ausgefallen? Möglicherweise ist ein Fehler aufgetreten, und das Entwicklungsteam arbeitet daran, ihn wieder funktionsfähig zu machen.> Möchten Sie wirklich, dass der Erfolg Ihrer Tests vom Zustand eines Servers abhängt, den Sie nicht kontrollieren?

Bei Ihren Tests sollte nicht bewertet werden, ob ein API-Server ausgeführt wird. Sie sollten testen, ob Ihr Code wie erwartet funktioniert.

Inprevious tutorial haben wir das Konzept von Scheinobjekten eingeführt und gezeigt, wie Sie sie zum Testen von Code verwenden können, der mit externen APIs interagiert. 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. Mit einem Mock-Server können Sie End-to-End-Tests durchführen. Sie können Ihre Anwendung verwenden und in Echtzeit aktuelles Feedback vom Mock-Server erhalten.

Wenn Sie die folgenden Beispiele durchgearbeitet haben, haben Sie einen einfachen Mock-Server und zwei Tests programmiert - einen, der den realen API-Server verwendet, und einen, der den Mock-Server verwendet. Beide Tests greifen auf denselben Dienst zu, eine API, die eine Liste von Benutzern abruft.

Note
Dieses Tutorial verwendet Python v3.5.1.

Anfangen

Folgen Sie zunächst dem AbschnittFirst steps des vorherigen Beitrags. Oder holen Sie sich den Code ausrepository. Stellen Sie sicher, dass der Test bestanden ist, bevor Sie fortfahren:

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

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

OK

Testen der Mock-API

Nach Abschluss der Einrichtung können Sie Ihren Mock-Server programmieren. Schreiben Sie einen Test, der das Verhalten beschreibt:

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)

Beachten Sie, dass es zunächst fast identisch mit dem tatsächlichen API-Test aussieht. Die URL hat sich geändert und zeigt jetzt auf einen API-Endpunkt auflocalhost, auf dem der Mock-Server ausgeführt wird.

So erstellen Sie einen Mock-Server in 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)

Erstellen Sie zunächst eine Unterklasse vonBaseHTTPRequestHandler. Diese Klasse erfasst die Anforderung und erstellt die Antwort für die Rückgabe. Überschreiben Sie die Funktiondo_GET(), um die Antwort für eine HTTP-GET-Anforderung zu erstellen. In diesem Fall geben Sie einfach einen OK-Status zurück. Schreiben Sie als Nächstes eine Funktion, um eine verfügbare Portnummer für den Mock-Server abzurufen.

Der nächste Codeblock konfiguriert den Server tatsächlich. Beachten Sie, wie der Code die Instanz einesHTTPServerinstanziiert und ihm eine Portnummer und einen Handler übergibt. Next, create a thread, damit der Server asynchron ausgeführt werden kann und Ihr Hauptprogramm-Thread mit ihm kommunizieren kann. Machen Sie den Thread zu einem Daemon, der den Thread anweist, anzuhalten, wenn das Hauptprogramm beendet wird. Starten Sie schließlich den Thread, um den Mock-Server für immer zu bedienen (bis die Tests abgeschlossen sind).

Erstellen Sie eine Testklasse und verschieben Sie die Testfunktion dorthin. Sie müssen eine zusätzliche Methode hinzufügen, um sicherzustellen, dass der Mock-Server gestartet wird, bevor einer der Tests ausgeführt wird. Beachten Sie, dass dieser neue Code in einer speziellen Funktion auf Klassenebene,setup_class(), gespeichert ist.

Führen Sie die Tests aus und beobachten Sie, wie sie bestehen:

$ nosetests --verbosity=2 project

Testen eines Dienstes, der auf die API trifft

Sie möchten wahrscheinlich mehr als einen API-Endpunkt in Ihrem Code aufrufen. Während Sie Ihre App entwerfen, werden Sie wahrscheinlich Servicefunktionen erstellen, um Anforderungen an eine API zu senden und die Antworten dann auf irgendeine Weise zu verarbeiten. Möglicherweise speichern Sie die Antwortdaten in einer Datenbank. Oder Sie übergeben die Daten an eine Benutzeroberfläche.

Refaktorieren Sie Ihren Code, um die fest codierte API-Basis-URL in eine Konstante zu ziehen. Fügen Sie diese Variable einerconstants.py-Datei hinzu:

project/constants.py

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

Kapselen Sie als Nächstes die Logik, um Benutzer von der API in eine Funktion abzurufen. Beachten Sie, wie neue URLs erstellt werden können, indem Sie einen URL-Pfad zur Basis verbinden.

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

Verschieben Sie den Mock-Server-Code aus der Feature-Datei in eine neue Python-Datei, damit er problemlos wiederverwendet werden kann. Fügen Sie dem Anforderungshandler eine bedingte Logik hinzu, um zu überprüfen, auf welchen API-Endpunkt die HTTP-Anforderung abzielt. Verbessern Sie die Antwort, indem Sie einige einfache Header-Informationen und eine grundlegende Antwortnutzlast hinzufügen. Der Servererstellungs- und Startcode kann in einer praktischen Methode,start_mock_server(), gekapselt werden.

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()

Ändern Sie nach Abschluss Ihrer Änderungen an der Logik die Tests, um die neue Servicefunktion zu verwenden. Aktualisieren Sie die Tests, um die erhöhten Informationen zu überprüfen, die vom Server zurückgegeben werden.

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(), [])

Beachten Sie, dass im Code vontest_mock_server.pyeine neue Technik verwendet wird. Die Zeileresponse = get_users() wird mit einer Funktionpatch.dict() aus der Bibliothekmockumbrochen.

Was macht diese Aussage?

Denken Sie daran, dass Sie die Funktionrequests.get()von der Funktionslogik in die Servicefunktionget_users()verschoben haben. Intern ruftget_users()requests.get() mit der VariablenUSERS_URL auf. Die Funktionpatch.dict()ersetzt vorübergehend den Wert der VariablenUSERS_URL. Tatsächlich geschieht dies nur im Rahmen der Anweisungwith. Nachdem dieser Code ausgeführt wurde, wird die VariableUSERS_URLauf ihren ursprünglichen Wert zurückgesetzt. Dieser Codepatchesist die URL zur Verwendung der Mock-Server-Adresse.

Führen Sie die Tests aus und beobachten Sie, wie sie bestanden werden.

$ 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

Überspringen von Tests, die die Real-API treffen

Wir haben mit diesem Tutorial begonnen, in dem die Vorteile des Testens eines Mock-Servers anstelle eines echten beschrieben werden. Ihr Code testet jedoch derzeit beide. Wie konfigurieren Sie die Tests so, dass der reale Server ignoriert wird? Die Python-Bibliothek "unittest" bietet verschiedene Funktionen, mit denen Sie Tests überspringen können. Sie können die bedingte Sprungfunktion "skipIf" zusammen mit einer Umgebungsvariablen verwenden, um die realen Servertests ein- und auszuschalten. Im folgenden Beispiel übergeben wir einen Tag-Namen, der ignoriert werden sollte:

$ 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)

Führen Sie die Tests aus und achten Sie darauf, wie der echte Servertest ignoriert wird:

$ 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)

Nächste Schritte

Nachdem Sie einen Mock-Server zum Testen Ihrer externen API-Aufrufe erstellt haben, können Sie dieses Wissen auf Ihre eigenen Projekte anwenden. Bauen Sie auf den hier erstellten einfachen Tests auf. Erweitern Sie die Funktionalität des Handlers, um das Verhalten der realen API genauer nachzuahmen.

Versuchen Sie die folgenden Übungen, um sich zu verbessern:

  • Gibt eine Antwort mit dem Status HTTP 404 (nicht gefunden) zurück, wenn eine Anforderung mit einem unbekannten Pfad gesendet wird.

  • Gibt eine Antwort mit dem Status HTTP 405 (Methode nicht zulässig) zurück, wenn eine Anforderung mit einer nicht zulässigen Methode (POST, DELETE, UPDATE) gesendet wird.

  • Geben Sie die tatsächlichen Benutzerdaten für eine gültige Anforderung an/users zurück.

  • Schreiben Sie Tests, um diese Szenarien zu erfassen.

Holen Sie sich den Code ausrepo.