0%

Abschlussprojekt: der Leselisten-Dienst und sein Client · Abschlussprojekt

Abschlussprojekt: Beweise es mit Tests

Client und Server stimmen bei allen elf Befehlen überein. Das beweist, dass der Code heute funktioniert. “Fertig” verlangt noch etwas mehr: Wenn jemand nächsten Monat eine Zeile ändert und eine Zeile des Vertrags kaputtmacht, bemerkt es ein Test. Die Tests, die mit dem Projekt gekommen sind, decken die Zeilen aus den Kapiteln 4 bis 10 ab, und keiner davon sieht sich die drei Zeilen an, die du in diesem Kapitel wiederhergestellt und gebaut hast. Diese Lektion fügt diese Tests hinzu.

Was Check my work tut

Check my work bewertet diese Lektion in zwei Teilen.

Zuerst wiederholt es jede Prüfung der Lektionen 1 bis 3, jede als eigene Prüfung, und stellt sicher, dass SERVICE_CONTRACT.md und README.md noch im Projekt sind und dass keine Datei in tests/ socket, http.server, http.client, irgendetwas aus urllib, requests oder httpx importiert oder practice-api nennt. Ein Test, der eine der Ausnahmeklassen von requests braucht, erreicht sie über den Client, als reading_client.requests.ConnectTimeout, so wie test_no_answer_exits_1 es schon tut.

Dann führt es deine Tests mit pytest aus, alle in tests/: einmal gegen deinen Code, so wie er ist, und einmal für jede von sechs Versionen deines Codes, in denen eine einen realistischen Fehler macht:

  1. handle beantwortet /v0/books/{id} mit einer 308, deren location /books ist, ohne die ID;

  2. cached_show fragt einen veralteten Eintrag ohne If-None-Match an;

  3. main gibt 0 zurück, wenn der Server mit einem Status 4xx oder 5xx geantwortet hat;

  4. call sendet seine Anfrage ohne Timeout;

  5. handle sendet mit seiner 304 den ganzen Body;

  6. create_book antwortet mit 201 ohne location.

Deine Tests müssen beim ersten Mal alle bestehen, und bei jedem anderen Mal muss mindestens einer fehlschlagen. Zwei der sechs werden von Tests erwischt, die du schon hast: test_an_error_status_exits_2_with_one_line schlägt gegen Nummer 3 fehl und test_call_sends_one_request_with_a_timeout gegen Nummer 4. Die anderen vier brauchen neue Tests.

Bei jedem Lauf stellt Check my work sicher, dass kein Test ein Netzwerk erreicht oder einen öffnet. reading_client.requests kann seine auslösen, aber nichts senden, also schlägt ein Test, der main ohne session aufruft, mit einer Meldung fehl, die ihm sagt, er solle die Session benutzen, die er bekommt. reading_server.http kann keinen Port binden. Jede Version wird über ihr eingesetzt, also rufe reading_server.handle(...) und reading_client.main(...) so auf, wie die Dateien es schon tun: Eine Zeile from reading_client import main oder eine Funktion, die oben in einer Datei in einem Namen gespeichert ist, behält das Original und verdeckt den Austausch.

Jeder Lauf hält bei seinem ersten fehlschlagenden Test an, und ein Lauf, der nach 3 Sekunden noch läuft, wird angehalten. Nach insgesamt 8 Sekunden startet kein weiterer Lauf, und die Meldung nennt die Versionen, die schon ausprobiert wurden.

Deine Tests bestehen. Check my work setzt ein cached_show ein, das nie If-None-Match sendet, und deine Tests bestehen immer noch. Was bedeutet das?

Sechs Tests

Schreibe mindestens einen Test für jede Hälfte jeder Zeile in “Not built yet”. Jeder Name funktioniert, und pytest findet jede Funktion, deren Name mit test_ beginnt. Das Projekt braucht insgesamt mindestens 15 Tests: die 9, die mitgekommen sind, und mindestens sechs weitere. Der Plan unten hat sieben, weil die Revalidierung zwei ist.

Die Server-Hälften kommen in tests/test_server.py, über handle und das dateieigene make_request(method, target, data=None, headers=None), wie Kapitel 5 es gezeigt hat. Hier ist der erste, für den alten Pfad:

def test_legacy_path_keeps_the_id_and_query():
    """Any method on /v0/books/2?x=1 gets a 308 to /books/2?x=1 and changes nothing."""
    state = reading_server.sample_state()
    for method in ["GET", "PUT", "DELETE"]:
        response = reading_server.handle(make_request(method, "/v0/books/2?x=1", None, WRITER), state)
        assert response["status"] == 308
        assert response["headers"]["location"] == "/books/2?x=1"
        assert response["body"] == b""
    assert state == reading_server.sample_state()

Der 304-Test fragt einmal nach /books/1, nimmt das etag aus dieser Antwort und sendet es in If-None-Match zurück: Die zweite Antwort muss eine 304 mit demselben Tag und dem Body b"" sein. Der location-Test sendet mit dem Token eines Schreibers ein Buch per POST und vergleicht die location der 201 mit /books/3, dem Pfad des dritten Buchs.

Die Client-Hälften kommen in tests/test_client.py, mit fakes.fake_session(replies, calls): Es beantwortet jede Anfrage mit der nächsten Antwort und hängt pro Aufruf ein an calls an, also ist calls[1]["headers"] das, was die zweite Anfrage gesendet hat. fakes.fake_response(status, data, headers=..., url=..., history=...) baut eine Antwort, und ihre headers unterscheiden nicht zwischen Groß- und Kleinschreibung, wie die von requests. Das dateieigene run_main(argv, session, tmp_path, now=0) führt main mit einer Cache-Datei innerhalb von tmp_path aus, dem Ordner, den pytest für jeden Test anlegt, wie in Python in der Praxis.

  • Die Revalidierung braucht zwei Tests. cached_show sendet If-None-Match in den headers= der Anfrage, nie auf der Session, also schaut dein Test dort nach. Einer zeigt dasselbe Buch bei 0 und bei 59 mit einer Antwort, die max-age=60 sagt, und sichert eine Anfrage und zwei ausgegebene Zeilen zu. Der andere speichert das Buch bei 0, zeigt es bei 60 erneut mit einer 304 als zweiter Antwort und sichert zu, dass calls[1]["headers"]["if-none-match"] das gespeicherte Tag ist.

  • Neue Bücher: Ein add, dessen Antwort url="http://shelf.test:8123/books" und location: /books/3 hat, muss ... at http://shelf.test:8123/books/3 ausgeben.

  • Alte Pfade: Ein update, dessen Antwort eine 308 in history hat, muss followed 308 from und die dieser 308 auf der Standardfehlerausgabe ausgeben und trotzdem 0 zurückgeben. Die Fixture capsys von pytest fängt beide Ströme ab: capsys.readouterr().err.

Eine Fake-Session, die keine Antwort mehr übrig hat, löst sofort einen Fehler aus, statt zu warten.

Deine Aufgabe

  1. Füge die drei Server-Tests zu tests/test_server.py und die vier Client-Tests zu tests/test_client.py hinzu.

  2. Führe die Testsuite aus, bis sie besteht:

python -m pytest -q
echo $?

Der eigene Exit-Status von pytest ist 0, wenn jeder Test bestanden hat, und 1, wenn einer fehlgeschlagen ist, also gibt echo $? 0 aus, sobald die Suite grün ist.

  1. Klicke auf Check my work.

Wenn Teil eins fehlschlägt, nennt seine Meldung die Regel aus den Lektionen 1 bis 3. Wenn Teil zwei sagt, dass deine Tests gegen eine fehlerhafte Version bestanden haben, nennt es den Fehler und die Art von Test, die ihn bemerken würde. Bitte Monty, diese Meldung mit dir neben deiner Testdatei zu lesen, ohne den Test zu schreiben.

Wenn jede Prüfung besteht, ist das Projekt fertig: ein Server und ein Client, die einen Vertrag erfüllen, und Tests, die sechs realistische Arten erwischen würden, ihn zu brechen. Die nächste Lektion holt es von der Website herunter.

Aufgabe

Beweise das Abschlussprojekt mit Tests, die eine kaputte Zeile bemerken würden.

Füge mindestens sechs Tests hinzu, einen oder mehr für jede Hälfte jeder Zeile in “Not built yet”: in tests/test_server.py, über reading_server.handle und make_request, die 308 für einen /v0/books/{id}-Pfad mit ihrer genauen location, die 304 ohne Body für ein passendes If-None-Match und die location der 201; in tests/test_client.py, über fakes.fake_session und run_main mit tmp_path, einen frischen Eintrag, der nichts sendet, einen veralteten Eintrag, der mit seinem gespeicherten Tag angefragt wird, die absolute nach add und eine followed-Zeile auf der Standardfehlerausgabe. Parametrisierte Tests sind in Ordnung. Behalte die 9 Tests, die mit dem Projekt gekommen sind, damit pytest mindestens 15 sammelt.

Jeder Test ruft die des Projekts über ihre auf (keine Zeile from reading_server import oder from reading_client import und kein Name auf Modulebene, der handle, create_book, cached_show, main oder call festhält), übergibt main eine Session und importiert nichts, was ein Netzwerk erreicht: kein socket, http.server, http.client, urllib, requests oder httpx. Behalte jede Server- und Client-Zeile aus den Lektionen 1 bis 3 bei. Führe im python -m pytest -q aus.

Klicke dann auf Check my work. Teil eins wiederholt jede Prüfung der Lektionen 1 bis 3 und prüft, dass SERVICE_CONTRACT.md und README.md da sind und was die Dateien in tests/ importieren. Teil zwei führt deine Tests mit pytest aus, wobei reading_client.requests nicht senden und reading_server.http keinen binden kann: einmal gegen deinen Code, wobei jeder Test bestehen muss, dann je einmal mit einer dieser Funktionen, ersetzt durch eine Version mit einem Fehler, in dieser Reihenfolge: handle, das eine 308 sendet, deren location die ID verliert; cached_show, das einen veralteten Eintrag ohne If-None-Match anfragt; main, das bei einem Fehlerstatus 0 zurückgibt; call ohne Timeout; handle, das mit seiner 304 einen Body sendet; create_book ohne location. Jedes Mal muss ein Test fehlschlagen. Jeder Lauf hält bei seinem ersten Fehlschlag an; ein Lauf, der nach 3 Sekunden noch läuft, wird angehalten, und nach insgesamt 8 Sekunden startet kein weiterer Lauf. Es liest dein Terminal nicht.