0%

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 , startet einen Thread oder wartet auf eine Uhr. Jeder Test ruft deine direkt mit Stellvertretern auf, läuft in Millisekunden und gibt jedes Mal dieselbe Antwort.

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 mit einer id, dessen send jede Nachricht geparst in seiner sent aufzeichnet. headers_with(*values) baut mit einem Authorization-Eintrag pro , so wie die Bibliothek sie baut, damit check_request sie mit get_all lesen kann.

tests/test_client.py enthält drei. snapshot(version, owls) gibt den -Text eines Snapshots mit einem Team zurück. 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 ablehnt.

Zwei weitere kommen von pytest selbst: Nenne einen eines Tests 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 take auf 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_request für /score ohne Token und mit einem falschen auf, was jeweils 401 ergibt; mit dem richtigen, was None ergibt; und für /watch ohne Token, was None ergibt.

  • Veraltete Snapshots. Führe scoreboard_client.main(["watch", "--url", ..., "--max-messages", "5"], connect=...) über connect_to mit 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ühre main(["score", ...]) mit --token-file, das auf sie zeigt, und refused_with(401) aus und sichere Exit-Status 1, 401 in der Standardfehlerausgabe und das Token in keinem der beiden Ausgabeströme zu. Das vorgegebene main liest diese Datei mit read_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 auf, 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:

  1. die Outbox eines neuen Zuschauers beginnt leer, also wird ihm bis zur nächsten Wertung nichts gesendet;

  2. der Beitritts-Snapshot eines neuen Zuschauers hat die Version der Tafel, aber jedes Team auf 0 Punkten;

  3. eine Wertung wird nur dem ersten Zuschauer angeboten;

  4. jeder darf werten, mit oder ohne Token;

  5. watch gibt einen Snapshot aus, der nicht neuer ist als der letzte;

  6. score meldet Erfolg, wenn der Handshake abgelehnt wird;

  7. eine Outbox behält den ältesten Snapshot statt des neuesten;

  8. 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 mit scoreboard_server.add_viewer hinzugefü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ässt scoreboard_server.apply_score die neue Version in beiden Outboxen.

  • test_only_the_token_holder_may_score: scoreboard_server.check_request lehnt /score ohne Token und mit einem falschen ab und lässt das richtige Token und /watch durch.

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 unter tmp_path und connect=refused_with(401) gibt main(["score", ...]) 1 zurück, schreibt 401 in die Standardfehlerausgabe und gibt das Token in keinem der beiden Ausgabeströme aus.

Verwende die Stellvertreter, die du bekommst; öffne keinen , starte keinen Thread und warte auf nichts. Behalte import scoreboard_server und import scoreboard_client und rufe ihre in jedem Test über das auf, niemals mit 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 lässt, ein watch, das einen veralteten Snapshot ausgibt, ein score, das bei einem abgelehnten 0 zurückgibt, ein 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.