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
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 id waarvan de send elk bericht, geparset, vastlegt in zijn sent. headers_with(*values) bouwt Authorization-regel per check_request ze kan lezen met get_all.
tests/test_client.py bevat er drie. snapshot(version, owls) geeft de 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
Er komen er nog twee van pytest zelf: noem een 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
takeop elke outbox de nieuwe versie geeft. Met één kijker kon een broadcast die te vroeg stopt het niet laten zien.Weigering. Roep
check_requestaan voor/scorezonder token en met een verkeerd token, die elk 401 geven; met het juiste, datNonegeeft; en voor/watchzonder token, datNonegeeft.Verouderd. Draai
scoreboard_client.main(["watch", "--url", ..., "--max-messages", "5"], connect=...)viaconnect_tomet 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, draaimain(["score", ...])met--token-filedat ernaar wijst enrefused_with(401), en controleer exitstatus 1,401op standard error, en het token op geen van beide streams. De meegeleverdemainleest dat bestand metread_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:
de outbox van een nieuwe kijker begint leeg, dus die krijgt niets toegestuurd tot de volgende score;
de snapshot bij binnenkomst van een nieuwe kijker heeft de versie van het bord, maar elk team op 0 punten;
een score wordt alleen aan de eerste kijker aangeboden;
iedereen mag scoren, met of zonder token;
watchdrukt een snapshot af die niet nieuwer is dan de laatste;scoremeldt succes als de handshake wordt geweigerd;een outbox bewaart de oudste snapshot in plaats van de nieuwste;
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 metscoreboard_server.add_viewerhet 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 laatscoreboard_server.apply_scorede nieuwe versie in beide outboxen achter.test_only_the_token_holder_may_score:scoreboard_server.check_requestweigert/scorezonder token en met een verkeerd token, en laat het juiste token en/watchdoor.
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 ondertmp_pathenconnect=refused_with(401)geeftmain(["score", ...])1 terug, schrijft401naar standard error, en drukt het token op geen van beide streams af.
Gebruik de stand-ins die je krijgt; open geen import scoreboard_server en import scoreboard_client, en roep hun 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 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.