0%

requests und httpx, jetzt, wo du Bescheid weißt · Übung

Einen Client ohne Server testen

Jede Prüfung von who_am_i und trail_httpx brauchte bisher einen Server: pl_http hat deinen gestartet, und die Bibliotheken haben mit ihm gesprochen. Tests sollten das nicht brauchen. In Networks with Python hast du -Code getestet, indem du ihm über eine eingeschleuste connect- einen Ersatz-Socket gegeben hast. In Python in der Praxis hast du eine requests-Funktion getestet, indem du ihr eine Ersatz-Session übergeben hast. Beide Bibliotheken in diesem Kapitel haben eine Stelle, an die ein Ersatz passt, und diese Lektion benutzt beide.

Ein Transport ist die Nahtstelle

Ein httpx-Client erledigt seine -Arbeit in zwei Schichten. Der Client baut die Anfrage, folgt , behält und wendet den Timeout an. Darunter nimmt ein Transport eine fertige Anfrage, besorgt eine Antwort dafür und gibt sie zurück. Normalerweise öffnet der Transport eine Verbindung. httpx.MockTransport ist ein Transport, der stattdessen eine Funktion von dir aufruft:

import httpx

def answer(request):
    if request.url.path == "/v0/books/2":
        return httpx.Response(308, headers={"location": "/books/2"})
    return httpx.Response(200, json={"id": 2, "title": "Café Society", "author": "Ana Ruiz"})

client = httpx.Client(transport=httpx.MockTransport(answer))

Die Funktion, oft Handler genannt, bekommt eine httpx.Request und gibt eine httpx.Response zurück. Die Anfrage enthält alles, was der Client gesendet hätte: request.method, request.url.path, request.headers (in denen host schon steht) und request.content, den Body als Bytes. httpx.Response(200, json=...) codiert das und fügt content-type: application/json für dich hinzu.

Alles oberhalb des Transports läuft wie gewohnt. Wenn answer die 308 zurückgibt und der Aufruf follow_redirects=True übergeben hat, löst der Client die location auf, baut die nächste Anfrage und ruft answer erneut mit dem Pfad /books/2 auf. Zu keinem Zeitpunkt wird ein Socket geöffnet; der Handler ist der ganze Server.

Eine Funktion an eine andere Funktion zu übergeben, tust du seit dem eingeschleusten connect in Networks with Python. Hier ist der Handler der , und MockTransport ruft ihn auf.

Was bekommt ein MockTransport-Handler, und was gibt er zurück?

Der Handler kann auch den Timeout sehen. httpx hängt ihn an jede Anfrage als request.extensions["timeout"], ein der vier Phasen. Mit dem HTTPX_TIMEOUT des Kurses, httpx.Timeout(2.0, connect=1.0), ist es {'connect': 1.0, 'read': 2.0, 'write': 2.0, 'pool': 2.0}; ohne angegebenen Timeout ist jede Phase die 5.0 von httpx. Ein Handler, der jede Anfrage, oder die Teile davon, die ihn interessieren, an eine anhängt, lässt einen Test danach prüfen, was gesendet wurde.

Wie kann ein Test den Timeout sehen, den ein httpx-Aufruf benutzt hat?

Die Ersatz-Session, die du kennst

Auch requests hat Transports, aber who_am_i nimmt eine ganze Session, also kann der Ersatz eine ganze Session sein. Öffne fakes.py. fake_session(replies, calls) gibt ein mit request, get, post, put und delete zurück, den , die who_am_i aufruft. Jeder Aufruf hängt ein Dictionary an deine Liste calls an, mit jedem Schlüsselwort, das requests bekommen hätte: method, url, params, headers, json, auth, timeout, allow_redirects. Dann antwortet er mit dem nächsten Element von replies, oder löst es aus, wenn dieses Element eine ist.

fake_response(status, data) baut die Antworten. Sie hat status_code, json(), headers ohne Unterscheidung von Groß- und Kleinschreibung und ein raise_for_status(), das bei 4xx und 5xx requests.HTTPError auslöst, also läuft who_am_i unverändert damit:

calls = []
replies = [fakes.fake_response(200, {"user": "ana"}), fakes.fake_response(200, {"user": "ana"})]
session = fakes.fake_session(replies, calls)

Nachdem who_am_i gelaufen ist, ist calls[0] das Login und calls[1] das /me. Ein Test kann jetzt etwas zusichern, das dir kein Server direkt zeigen würde: die in auth übergebenen Zugangsdaten und den Timeout jedes Aufrufs.

Über das Modul testen

Auch deine Tests werden geprüft. Check my work führt sie gegen deine eigene side_by_side.py aus und dann noch einmal, wobei eine Funktion durch eine Version mit einem Fehler ersetzt ist, um zu sehen, ob ein Test es bemerkt. Es setzt jede Version ein, indem es sie auf dem setzt, side_by_side.trail_httpx = .... Ein Test, der side_by_side.trail_httpx(...) aufruft, schlägt den Namen in dem Moment nach, in dem er läuft, und trifft so auf die Version, die gerade da ist. Eine Zeile from side_by_side import trail_httpx kopiert das Original in einen eigenen Namen deiner Testdatei, und der Austausch erreicht ihn nie.

Die Versionen mit Fehlern sind die, nach denen die beiden Tests dieser Lektion benannt sind: ein trail_httpx, das der Weiterleitung nicht folgt, und ein who_am_i, dessen GET /me keinen Timeout übergibt. Ein Test, der nur den Timeout des Login-Aufrufs prüft, besteht bei der zweiten, also prüfe beide Aufrufe.

Warum side_by_side.trail_httpx(...) aufrufen, statt den Namen mit from side_by_side import trail_httpx zu importieren?

Deine Aufgabe

Beide Funktionen nehmen ihren Ersatz nach dem * in ihrer Definition, also übergibt ein Test ihn mit Namen, als client=client oder session=session:

def trail_httpx(url, *, client=None, timeout=HTTPX_TIMEOUT):
    ...

def who_am_i(base_url, user, password, *, session, timeout=TIMEOUT):
    ...

Ersetze in tests/test_side_by_side.py die beiden Platzhalter, die assert False, "not written yet" sagen:

  1. test_trail_httpx_follows_a_redirect: Schreibe einen Handler, der für einen Pfad mit 308 und einer location antwortet und für den nächsten mit 200, erstelle einen Client mit httpx.MockTransport(handler), rufe side_by_side.trail_httpx(url, client=client) auf und sichere zu, dass die Spur aus beiden Zeilen besteht, die 308 zuerst.

  2. test_who_am_i_sends_credentials_and_timeouts: Gib side_by_side.who_am_i eine fakes.fake_session mit zwei Antworten und einen Timeout deiner Wahl und sichere dann den , das auth des Login-Aufrufs und den timeout beider Aufrufe zu.

Namen und zusätzliche Tests überlassen wir dir. Jeder Test muss der Funktion seinen eigenen Ersatz übergeben; ein Test, der client oder session weglässt, bräuchte ein Netzwerk und schlägt hier fehl. Führe nur diese Datei aus, denn tests/test_client.py wartet auf Lektion 6:

python -m pytest -q tests/test_side_by_side.py

Es sollte 2 passed melden, oder mehr, wenn du mehr geschrieben hast. Klicke dann auf Check my work. Lektion 5 sieht sich die Zeilen an, die beide Bibliotheken senden, obwohl niemand danach gefragt hat.

Aufgabe

Teste trail_httpx und who_am_i ohne Server.

Ersetze in tests/test_side_by_side.py die beiden Platzhalter:

  1. test_trail_httpx_follows_a_redirect: Eine Handler- antwortet für einen Pfad mit 308 und einer location und für den Pfad, auf den sie zeigt, mit 200. Erstelle einen Client mit httpx.Client(transport=httpx.MockTransport(handler)), rufe side_by_side.trail_httpx(url, client=client) auf und sichere zu, dass die Spur beide Zeilen enthält, die 308 zuerst.

  2. test_who_am_i_sends_credentials_and_timeouts: Rufe side_by_side.who_am_i(...) mit fakes.fake_session(replies, calls) und einem Timeout deiner Wahl auf. Sichere zu, was es zurückgibt, das auth des Login-Aufrufs und den timeout beider aufgezeichneten Aufrufe, des GET /me ebenso wie des POST /login.

Behalte import side_by_side und rufe die Funktionen in jedem Test über das auf; füge keine Zeile from side_by_side import hinzu und speichere keine der beiden Funktionen oben in der Datei in einem Namen. Übergib bei jedem Aufruf einen Client oder eine Session. Importiere keine Module socket, http oder urllib. Weitere Tests sind willkommen.

Im führt python -m pytest -q tests/test_side_by_side.py diese Tests aus; wenn beide geschrieben sind, meldet es 2 passed oder mehr.

Klicke dann auf Check my work. Es liest die Datei und führt sie dann dreimal mit pytest aus: gegen deine eigene side_by_side.py, gegen ein trail_httpx, das nicht folgt, und gegen ein who_am_i, dessen GET /me keinen Timeout hat. Deine Tests müssen gegen die erste alle bestehen, und gegen jede der anderen muss mindestens einer fehlschlagen. Während sie laufen, weigern sich requests und httpx in side_by_side, irgendetwas zu senden, also schlägt ein Test fehl, der seinen Ersatz vergisst. Ein Lauf, der länger als 3 Sekunden dauert, wird angehalten und gemeldet. Nichts aus deinem Terminal wird bewertet.