Abschlussprojekt: Eine Live-Anzeigetafel · Abschlussprojekt
Abschlussprojekt: Mit Tests beweisen
Der Server und der Client halten jeweils ihre Hälfte des Vertrags ein, und bisher sind der einzige Beweis Check my work und die Protokolle von pl_ws.py. Ein Projekt, das du jemand anderem übergibst, braucht einen eigenen Beweis: Tests, die mit einem Befehl laufen und bemerken würden, wenn eine Regel bricht. Eine Regel, die niemand testet, ist eine Regel, die die nächste Änderung brechen kann, ohne dass es jemand sieht.
Nichts in dieser Lektion öffnet einen
Die Stellvertreter, die du bekommst
tests/test_scoreboard.py enthält schon zwei Helfer. make_connection(conn_id) ist der Stellvertreter-Zuschauer aus Kapitel 7: ein id, dessen send jede Nachricht geparst in seiner sent aufzeichnet. headers_with(*values) baut Authorization-Eintrag pro check_request sie mit get_all lesen kann.
tests/test_client.py enthält drei. snapshot(version, owls) gibt den connect_to(messages, calls) gibt einen Stellvertreter für das connect der Bibliothek zurück: Seine Verbindung gibt messages eine nach der anderen heraus und löst dann das Schließen mit 1000 aus, das die Bibliothek auslösen würde, und sie zeichnet die Optionen auf, mit denen connect aufgerufen wurde, und jede gesendete Wertung. Er gibt contextlib.nullcontext(connection) zurück, was dem with-Block des Clients etwas gibt, das er betreten kann und das den Stellvertreter zurückgibt. refused_with(status) gibt ein connect zurück, das für diesen Status InvalidStatus auslöst, wie es die Bibliothek tut, wenn ein Server den
Zwei weitere kommen von pytest selbst: Nenne einen capsys, und pytest übergibt ein Objekt, dessen readouterr() zurückgibt, was der Test bisher ausgegeben hat, als out und err; nenne ihn tmp_path, und du bekommst einen frischen Ordner für Dateien, die der Test schreibt. Beide hast du in Python in der Praxis kennengelernt.
Fünf Tests
Jeder Test bereitet eine Tafel oder einen Stellvertreter vor, handelt einmal und sichert zu, was der Vertrag sagt. Der Beitrittstest zum Beispiel:
def test_a_new_viewer_starts_with_the_whole_board():
board = scoreboard_server.make_board(["Owls", "Foxes"])
scoreboard_server.apply_score(board, {"type": "score", "team": "Owls", "points": 4})
outbox = scoreboard_server.add_viewer(board, make_connection("v1"))
first = scoreboard_server.take(outbox)
assert first["version"] == 1
assert first["scores"] == {"Owls": 4, "Foxes": 0}
Die anderen vier, in Worten:
Broadcast. Füge zwei Zuschauer hinzu, wende eine Wertung an und sichere zu, dass
takeauf jeder Outbox die neue Version liefert. Mit einem einzigen Zuschauer könnte sich ein Broadcast, der zu früh aufhört, nicht zeigen.Ablehnung. Rufe
check_requestfür/scoreohne Token und mit einem falschen auf, was jeweils 401 ergibt; mit dem richtigen, wasNoneergibt; und für/watchohne Token, wasNoneergibt.Veraltete Snapshots. Führe
scoreboard_client.main(["watch", "--url", ..., "--max-messages", "5"], connect=...)überconnect_tomit den Versionen 3, 2, 3, 4 aus und sichere zu, dass genau zwei Zeilen ausgegeben wurden, für 3 und 4, und dass es 0 zurückgegeben hat.Ein abgelehntes Token. Schreibe ein Token in eine Datei unter
tmp_path, führemain(["score", ...])mit--token-file, das auf sie zeigt, undrefused_with(401)aus und sichere Exit-Status 1,401in der Standardfehlerausgabe und das Token in keinem der beiden Ausgabeströme zu. Das vorgegebenemainliest diese Datei mitread_token, das aus drei Zeilen besteht:
def read_token(path):
with open(path, encoding="utf-8") as file:
return file.read().strip()
Rufe jede Funktion über ihr scoreboard_server.add_viewer(...) und scoreboard_client.main(...), nie über einen mit from importierten Namen. Kapitel 7 hat den Grund genannt, und hier ist er noch wichtiger: Check my work setzt über das Modul andere Versionen deiner Funktionen ein, und ein mit from importierter Name würde das Original behalten.
Warum verwenden diese Tests Stellvertreter-Verbindungen, statt den Server zu starten und sich mit ihm zu verbinden?
Was Check my work tut
Check my work führt deine Tests zweimal aus. Zuerst so, wie sie sind, gegen deinen eigenen Code: Jeder Test muss bestehen. Dann einmal für jede von acht Versionen deines Codes, die je einen realistischen Fehler machen, wobei eine Funktion ersetzt ist:
die Outbox eines neuen Zuschauers beginnt leer, also wird ihm bis zur nächsten Wertung nichts gesendet;
der Beitritts-Snapshot eines neuen Zuschauers hat die Version der Tafel, aber jedes Team auf 0 Punkten;
eine Wertung wird nur dem ersten Zuschauer angeboten;
jeder darf werten, mit oder ohne Token;
watchgibt einen Snapshot aus, der nicht neuer ist als der letzte;scoremeldet Erfolg, wenn der Handshake abgelehnt wird;eine Outbox behält den ältesten Snapshot statt des neuesten;
ein Zuschauer wird nur dann aus dem Register entfernt, wenn seine Verbindung schließt.
Gegen jede davon muss mindestens einer deiner Tests scheitern. Drei der acht erwischen auch Tests, die du schon aus Kapitel 8 hast: die leere Outbox (ein Zuschauer, dem nichts gesendet wird, wartet, und der Test zum Verlassen bemerkt es), den ältesten Snapshot und das Entfernen. Den zweiten erwischt nur ein Beitrittstest, der die Punktestände prüft, also prüfe sie. Jeder Lauf wird gestoppt, wenn ein Test nach drei Sekunden noch läuft, und ein gestoppter Lauf einer fehlerhaften Version zählt als bemerkt.
Diese Tests beweisen die Regeln einen Aufruf nach dem anderen. Sie führen keinen Thread aus, können also den Wettlauf beim Beitritt oder am Ende des Quiz nicht zeigen; diesen Teil beweisen die eigenen Prüfungen von Check my work, die du in den Lektionen 1 bis 3 schon bestanden hast.
Check my work ersetzt broadcast durch eine Version, die den neuen Snapshot nur dem ersten Zuschauer anbietet, und deine Tests bestehen trotzdem. Was fehlt?
Deine Aufgabe
Ersetze die drei Stubs am Ende von tests/test_scoreboard.py, die Lektion 4 nennt, und die zwei in tests/test_client.py durch echte Tests. Behalte ihre Namen: Der vierte Stub in tests/test_scoreboard.py ist für Lektion 5, und Check my work lässt ihn anhand seines Namens aus. Du darfst eigene Tests und Helfer hinzufügen.
python -m pytest -q
echo $?
Wenn deine fünf Tests geschrieben sind, meldet pytest 1 failed und 12 passed, und echo $? gibt weiterhin 1 aus: Der eine, der scheitert, ist der Stub für Lektion 5, test_the_last_update_is_sent_before_the_close. Lektion 5 schreibt ihn, führt den ganzen Quizabend aus und prüft alles auf einmal.
Aufgabe
Ersetze fünf assert False-Stubs durch Tests und behalte ihre Namen. Behalte jeden anderen Test und Helfer, und lass den Stub für Lektion 5, test_the_last_update_is_sent_before_the_close, wie er ist.
In tests/test_scoreboard.py:
test_a_new_viewer_starts_with_the_whole_board: Nach einer Wertung findet ein mitscoreboard_server.add_viewerhinzugefügter Zuschauer die ganze Tafel in seiner Outbox, Version 1 und die Punkte jedes Teams.test_a_score_is_offered_to_every_viewer: Mit zwei hinzugefügten Zuschauern lässtscoreboard_server.apply_scoredie neue Version in beiden Outboxen.test_only_the_token_holder_may_score:scoreboard_server.check_requestlehnt/scoreohne Token und mit einem falschen ab und lässt das richtige Token und/watchdurch.
In tests/test_client.py:
test_watch_skips_a_snapshot_that_is_not_newer:scoreboard_client.main(["watch", ...], connect=connect_to(...))mit den Versionen 3, 2, 3, 4 gibt nur die Zeilen für 3 und 4 aus und gibt 0 zurück.test_a_refused_token_exits_1_and_says_why: Mit einer Token-Datei untertmp_pathundconnect=refused_with(401)gibtmain(["score", ...])1 zurück, schreibt401in die Standardfehlerausgabe und gibt das Token in keinem der beiden Ausgabeströme aus.
Verwende die Stellvertreter, die du bekommst; öffne keinen import scoreboard_server und import scoreboard_client und rufe ihre from ... import.
Führe python -m pytest -q aus, bis nur noch der Stub für Lektion 5 scheitert. Klicke dann auf Check my work. Die Prüfung führt deine Tests in diesem Prozess aus, ohne den Stub für Lektion 5: einmal gegen deinen eigenen Code, wobei mindestens 10 Tests gesammelt werden und alle bestehen müssen, dann je einmal mit einer von acht Funktionen ersetzt durch eine Version, die einen realistischen Fehler macht (die Outbox eines neuen Zuschauers, die leer beginnt, ein Beitritts-Snapshot mit der Version der Tafel, aber jedem Team auf 0 Punkten, ein Broadcast nur an den ersten Zuschauer, ein check_request, das jeden watch, das einen veralteten Snapshot ausgibt, ein score, das bei einem abgelehnten offer, das den ältesten Snapshot behält, und ein viewer_handler, das den Zuschauer nur entfernt, wenn seine Verbindung schließt). Gegen jede davon muss mindestens ein Test scheitern. Ein Lauf, der nach 3 Sekunden noch läuft, wird gestoppt, und gegen eine fehlerhafte Version zählt das als bemerkt. Nach 8 Sekunden startet kein neuer Lauf mehr. Beliebige Testnamen dürfen hinzugefügt werden, und ebenso Helfer.