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 connect-
Ein Transport ist die Nahtstelle
Ein httpx-Client erledigt seine 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 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 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 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
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 request, get, post, put und delete zurück, den 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
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 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:
test_trail_httpx_follows_a_redirect: Schreibe einen Handler, der für einen Pfad mit 308 und einerlocationantwortet und für den nächsten mit 200, erstelle einen Client mithttpx.MockTransport(handler), rufeside_by_side.trail_httpx(url, client=client)auf und sichere zu, dass die Spur aus beiden Zeilen besteht, die 308 zuerst.test_who_am_i_sends_credentials_and_timeouts: Gibside_by_side.who_am_ieinefakes.fake_sessionmit zwei Antworten und einen Timeout deiner Wahl und sichere dann den, das authdes Login-Aufrufs und dentimeoutbeider 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:
test_trail_httpx_follows_a_redirect: Eine Handler-antwortet für einen Pfad mit 308 und einer locationund für den Pfad, auf den sie zeigt, mit 200. Erstelle einen Client mithttpx.Client(transport=httpx.MockTransport(handler)), rufeside_by_side.trail_httpx(url, client=client)auf und sichere zu, dass die Spur beide Zeilen enthält, die 308 zuerst.test_who_am_i_sends_credentials_and_timeouts: Rufeside_by_side.who_am_i(...)mitfakes.fake_session(replies, calls)und einem Timeout deiner Wahl auf. Sichere zu, was es zurückgibt, dasauthdes Login-Aufrufs und dentimeoutbeider aufgezeichneten Aufrufe, desGET /meebenso wie desPOST /login.
Behalte import side_by_side und rufe die Funktionen in jedem Test über das 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 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 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.