0%

Eindproject: een live scorebord · eindproject

Eindproject: Bewijs het met tests

De server en de client houden zich elk aan hun helft van het contract, en tot nu toe is het enige bewijs Check my work en de transcripten van pl_ws.py. Een project dat je aan iemand anders overdraagt, heeft een eigen bewijs nodig: tests die met één opdracht draaien en zouden merken als een regel brak. Een regel die niemand test, is een regel die de volgende verandering kan breken zonder dat iemand het ziet.

Niets in deze les opent een , start een thread of wacht op een klok. Elke test roept je direct aan met stand-ins, draait in milliseconden en geeft elke keer hetzelfde antwoord.

De stand-ins die je krijgt

tests/test_scoreboard.py bevat al twee hulpfuncties. make_connection(conn_id) is de stand-in-kijker uit hoofdstuk 7: een met een id waarvan de send elk bericht, geparset, vastlegt in zijn sent. headers_with(*values) bouwt met één Authorization-regel per , zoals de bibliotheek ze bouwt, zodat check_request ze kan lezen met get_all.

tests/test_client.py bevat er drie. snapshot(version, owls) geeft de -tekst terug van een snapshot met één team. connect_to(messages, calls) geeft een stand-in terug voor de connect van de bibliotheek: de verbinding ervan deelt messages één voor één uit en werpt daarna de close met 1000 op die de bibliotheek zou opwerpen, en hij legt de opties vast waarmee connect werd aangeroepen en elke verstuurde score. Hij geeft contextlib.nullcontext(connection) terug, wat het with-blok van de client iets geeft om binnen te gaan dat de stand-in teruggeeft. refused_with(status) geeft een connect terug die InvalidStatus opwerpt voor die status, zoals de bibliotheek doet als een server de weigert.

Er komen er nog twee van pytest zelf: noem een van een test capsys en pytest geeft een object mee waarvan readouterr() teruggeeft wat de test tot nu toe afdrukte, als out en err; noem hem tmp_path en je krijgt een verse map voor bestanden die de test schrijft. Je kwam ze allebei tegen in Python in Practice.

Vijf tests

Elke test zet een bord of een stand-in klaar, handelt één keer, en controleert wat het contract zegt. De test voor binnenkomen, bijvoorbeeld:

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}

De andere vier, in woorden:

  • Broadcast. Voeg twee kijkers toe, verwerk één score, en controleer dat take op elke outbox de nieuwe versie geeft. Met één kijker kon een broadcast die te vroeg stopt het niet laten zien.

  • Weigering. Roep check_request aan voor /score zonder token en met een verkeerd token, die elk 401 geven; met het juiste, dat None geeft; en voor /watch zonder token, dat None geeft.

  • Verouderd. Draai scoreboard_client.main(["watch", "--url", ..., "--max-messages", "5"], connect=...) via connect_to met versies 3, 2, 3, 4, en controleer dat er precies twee regels werden afgedrukt, voor 3 en 4, en dat het 0 teruggaf.

  • Een geweigerd token. Schrijf een token naar een bestand onder tmp_path, draai main(["score", ...]) met --token-file dat ernaar wijst en refused_with(401), en controleer exitstatus 1, 401 op standard error, en het token op geen van beide streams. De meegeleverde main leest dat bestand met read_token, dat drie regels lang is:

def read_token(path):
    with open(path, encoding="utf-8") as file:
        return file.read().strip()

Roep elke functie aan via haar , scoreboard_server.add_viewer(...) en scoreboard_client.main(...), nooit via een naam die met from is geïmporteerd. Hoofdstuk 7 gaf de reden, en die doet er hier nog meer toe: Check my work wisselt andere versies van je functies in via de module, en een naam die met from is geïmporteerd, zou het origineel houden.

Waarom gebruiken deze tests stand-in-verbindingen in plaats van de server te starten en er verbinding mee te maken?

Wat Check my work doet

Check my work draait je tests in twee rondes. Eerst zoals ze zijn, tegen je eigen code: elke test moet slagen. Daarna één keer voor elk van acht versies van je code die elk één realistische fout maken, met één functie vervangen:

  1. de outbox van een nieuwe kijker begint leeg, dus die krijgt niets toegestuurd tot de volgende score;

  2. de snapshot bij binnenkomst van een nieuwe kijker heeft de versie van het bord, maar elk team op 0 punten;

  3. een score wordt alleen aan de eerste kijker aangeboden;

  4. iedereen mag scoren, met of zonder token;

  5. watch drukt een snapshot af die niet nieuwer is dan de laatste;

  6. score meldt succes als de handshake wordt geweigerd;

  7. een outbox bewaart de oudste snapshot in plaats van de nieuwste;

  8. een kijker wordt alleen uit het register verwijderd als zijn verbinding sluit.

Tegen elk hiervan moet minstens één van je tests falen. Drie van de acht worden ook betrapt door tests die je al hebt uit hoofdstuk 8: de lege outbox (een kijker die niets krijgt, wacht, en de exittest merkt dat), de oudste snapshot en het verwijderen. De tweede wordt alleen betrapt door een test voor binnenkomen die de scores controleert, dus controleer die. Elke run wordt gestopt als er na drie seconden nog een test draait, en een gestopte run van een foutieve versie telt als opgemerkt.

Deze tests bewijzen de regels één aanroep tegelijk. Ze draaien geen thread, dus ze kunnen de race bij het binnenkomen of aan het eind van de quiz niet laten zien; dat deel wordt bewezen door de eigen controles van Check my work, die je in les 1 tot en met 3 al hebt gehaald.

Check my work vervangt broadcast door een versie die de nieuwe snapshot alleen aan de eerste kijker aanbiedt, en je tests slagen nog steeds. Wat ontbreekt er?

Je opdracht

Vervang de drie stubs aan het eind van tests/test_scoreboard.py die les 4 noemt en de twee in tests/test_client.py door echte tests. Houd hun namen: de vierde stub in tests/test_scoreboard.py is voor les 5, en Check my work laat die op naam weg. Je mag eigen tests en hulpfuncties toevoegen.

python -m pytest -q
echo $?

Als je vijf tests geschreven zijn, meldt pytest 1 failed en 12 passed, en echo $? drukt nog steeds 1 af: degene die faalt, is de stub van les 5, test_the_last_update_is_sent_before_the_close. Les 5 schrijft hem, draait de hele quizavond en controleert alles in één keer.

Opdracht

Vervang vijf stubs met assert False door tests, en houd hun namen. Houd elke andere test en hulpfunctie, en laat de stub van les 5, test_the_last_update_is_sent_before_the_close, zoals hij is.

In tests/test_scoreboard.py:

  • test_a_new_viewer_starts_with_the_whole_board: na één score vindt een kijker die is toegevoegd met scoreboard_server.add_viewer het hele bord in zijn outbox, versie 1 en de punten van elk team.

  • test_a_score_is_offered_to_every_viewer: met twee toegevoegde kijkers laat scoreboard_server.apply_score de nieuwe versie in beide outboxen achter.

  • test_only_the_token_holder_may_score: scoreboard_server.check_request weigert /score zonder token en met een verkeerd token, en laat het juiste token en /watch door.

In tests/test_client.py:

  • test_watch_skips_a_snapshot_that_is_not_newer: scoreboard_client.main(["watch", ...], connect=connect_to(...)) die versies 3, 2, 3, 4 krijgt, drukt alleen de regels voor 3 en 4 af en geeft 0 terug.

  • test_a_refused_token_exits_1_and_says_why: met een tokenbestand onder tmp_path en connect=refused_with(401) geeft main(["score", ...]) 1 terug, schrijft 401 naar standard error, en drukt het token op geen van beide streams af.

Gebruik de stand-ins die je krijgt; open geen , start geen thread en wacht nergens op. Houd import scoreboard_server en import scoreboard_client, en roep hun binnen elke test aan via de , nooit met from ... import.

Draai python -m pytest -q totdat alleen de stub van les 5 faalt. Klik daarna op Check my work. Die draait je tests in dit proces, met de stub van les 5 weggelaten: één keer tegen je eigen code, waarbij minstens 10 tests moeten worden verzameld en ze allemaal moeten slagen, en daarna één keer met elk van acht functies vervangen door een versie die één realistische fout maakt (een outbox van een nieuwe kijker die leeg begint, een snapshot bij binnenkomst met de versie van het bord maar elk team op 0 punten, een broadcast alleen naar de eerste kijker, een check_request die iedereen laat scoren, een watch die een verouderde snapshot afdrukt, een score die 0 teruggeeft bij een geweigerde , een offer die de oudste snapshot bewaart, en een viewer_handler die de kijker alleen verwijdert als zijn verbinding sluit). Tegen elk ervan moet minstens één test falen. Een run die na 3 seconden nog loopt, wordt gestopt, en tegen een foutieve versie telt dat als opgemerkt. Er start geen nieuwe run meer zodra er 8 seconden voorbij zijn. Je mag tests met elke naam toevoegen, en ook hulpfuncties.