0%

Eindproject: de leeslijstservice en -client · eindproject

Eindproject: Bewijs het met tests

Client en server zijn het eens over alle elf opdrachten. Dat bewijst dat de code vandaag werkt. “Af” vraagt nog één ding: als iemand volgende maand één regel veranderde en een rij van het contract brak, zou een test dat merken. De tests die met het project meekwamen, dekken de rijen uit hoofdstuk 4 tot en met 10, en geen ervan kijkt naar de drie rijen die je in dit hoofdstuk hebt hersteld en gebouwd. Deze les voegt die tests toe.

Wat Check my work doet

Check my work beoordeelt deze les in twee delen.

Eerst herhaalt hij elke controle van les 1 tot en met 3, elk als eigen controle, en kijkt hij of SERVICE_CONTRACT.md en README.md nog in het project staan en of geen enkel bestand in tests/ socket, http.server, http.client, iets uit urllib, requests of httpx importeert, of practice-api noemt. Een test die een van de exceptionklassen van requests nodig heeft, bereikt die via de client, als reading_client.requests.ConnectTimeout, zoals test_no_answer_exits_1 al doet.

Dan voert hij je tests uit met pytest, heel tests/: één keer tegen je code zoals die is, en één keer voor elk van zes versies van je code waarin één één realistische fout maakt:

  1. handle antwoordt op /v0/books/{id} met een 308 waarvan de location /books is, zonder het id;

  2. cached_show vraagt een verlopen item op zonder If-None-Match;

  3. main geeft 0 terug als de server antwoordde met een status 4xx of 5xx;

  4. call stuurt zijn verzoek zonder timeout;

  5. handle stuurt de hele body mee met zijn 304;

  6. create_book antwoordt 201 zonder location.

Je tests moeten de eerste keer allemaal slagen, en elke andere keer moet er minstens één mislukken. Twee van de zes worden gevangen door tests die je al hebt: test_an_error_status_exits_2_with_one_line mislukt bij nummer 3, en test_call_sends_one_request_with_a_timeout bij nummer 4. De andere vier hebben nieuwe tests nodig.

Tijdens elke run zorgt Check my work ervoor dat geen test een netwerk bereikt of een opent. reading_client.requests kan zijn opwerpen maar niets versturen, dus een test die main aanroept zonder session mislukt met een melding die zegt dat hij de sessie moet gebruiken die hij krijgt. reading_server.http kan geen poort binden. Elke versie wordt via haar ingewisseld, dus roep reading_server.handle(...) en reading_client.main(...) aan zoals de bestanden al doen: een regel from reading_client import main, of een functie die bovenaan een bestand in een naam wordt bewaard, houdt het origineel vast en verbergt de wissel.

Elke run stopt bij zijn eerste mislukte test, en een run die na 3 seconden nog bezig is, wordt gestopt. Na in totaal 8 seconden begint er geen nieuwe run meer, en de melding noemt de versies die al geprobeerd zijn.

Je tests slagen. Check my work wisselt een cached_show in die nooit If-None-Match stuurt, en je tests slagen nog steeds. Wat betekent dat?

Zes tests

Schrijf minstens één test voor elke helft van elke rij in “Not built yet”. Elke naam werkt, en pytest vindt elke functie waarvan de naam begint met test_. Het project heeft in totaal minstens 15 tests nodig: de 9 die meekwamen en minstens zes nieuwe. Het plan hieronder heeft er zeven, omdat hervalidatie er twee waard is.

De serverhelften komen in tests/test_server.py, via handle en de eigen make_request(method, target, data=None, headers=None) van het bestand, zoals hoofdstuk 5 je leerde. Hier is de eerste, voor het oude pad:

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()

De 304-test vraagt /books/1 één keer op, neemt de etag uit dat antwoord en stuurt die terug in If-None-Match: het tweede antwoord moet een 304 zijn met dezelfde tag en de body b"". De location-test post een boek met het token van de schrijver en vergelijkt de location van de 201 met /books/3, het pad van het derde boek.

De clienthelften komen in tests/test_client.py, met fakes.fake_session(replies, calls): die beantwoordt elk verzoek met het volgende antwoord en voegt per aanroep een toe aan calls, dus calls[1]["headers"] is wat het tweede verzoek stuurde. fakes.fake_response(status, data, headers=..., url=..., history=...) bouwt een antwoord, en de headers daarvan zijn niet hoofdlettergevoelig, net als die van requests. De eigen run_main(argv, session, tmp_path, now=0) van het bestand voert main uit met een cachebestand binnen tmp_path, de map die pytest voor elke test maakt, zoals in Python in Practice.

  • Hervalidatie heeft twee tests nodig. cached_show stuurt If-None-Match in de headers= van het verzoek, nooit op de sessie, dus daar kijkt je test. De ene toont hetzelfde boek op 0 en 59 met een antwoord dat max-age=60 zegt, en controleert dat er één verzoek was en twee afgedrukte regels. De andere slaat het boek op bij 0, toont het opnieuw op 60 met een 304 als tweede antwoord, en controleert dat calls[1]["headers"]["if-none-match"] de opgeslagen tag is.

  • Nieuwe boeken: een add waarvan het antwoord url="http://shelf.test:8123/books" en location: /books/3 heeft, moet ... at http://shelf.test:8123/books/3 afdrukken.

  • Oude paden: een update waarvan het antwoord een 308 in history heeft, moet followed 308 from en de van die 308 afdrukken op standard error, en toch 0 teruggeven. De capsys-fixture van pytest vangt beide streams op: capsys.readouterr().err.

Een fake sessie zonder antwoorden meer werpt meteen een fout op in plaats van te wachten.

Je opdracht

  1. Voeg de drie servertests toe aan tests/test_server.py en de vier clienttests aan tests/test_client.py.

  2. Voer de testsuite uit tot hij slaagt:

python -m pytest -q
echo $?

De eigen exitstatus van pytest is 0 als elke test slaagde en 1 als er een mislukte, dus echo $? drukt 0 af zodra de suite groen is.

  1. Klik op Check my work.

Als deel één mislukt, noemt de melding de regel uit les 1 tot en met 3. Als deel twee zegt dat je tests slaagden tegen een foute versie, noemt hij de fout en het soort test dat die zou merken. Vraag Monty om die melding samen met je naast je testbestand te lezen, zonder de test te schrijven.

Als elke controle slaagt, is het project af: een server en een client die aan één contract voldoen, en tests die zes realistische manieren om het te breken zouden vangen. De volgende les haalt het van de site.

Opdracht

Bewijs het eindproject met tests die een gebroken rij zouden merken.

Voeg minstens zes tests toe, één of meer voor elke helft van elke rij in “Not built yet”: in tests/test_server.py, via reading_server.handle en make_request, de 308 voor een /v0/books/{id}-pad met zijn exacte location, de 304 zonder body voor een overeenkomende If-None-Match, en de location van de 201; in tests/test_client.py, via fakes.fake_session en run_main met tmp_path, een vers item dat niets verstuurt, een verlopen item dat met zijn opgeslagen tag wordt gevraagd, de absolute na add, en een followed-regel op standard error. Geparametriseerde tests zijn prima. Houd de 9 tests die met het project meekwamen, zodat pytest er minstens 15 verzamelt.

Elke test roept de van het project aan via hun (geen regel from reading_server import of from reading_client import, en geen naam op moduleniveau die handle, create_book, cached_show, main of call vasthoudt), geeft een sessie mee aan main, en importeert niets dat een netwerk bereikt: geen socket, http.server, http.client, urllib, requests of httpx. Houd elke server- en clientrij uit les 1 tot en met 3. Voer python -m pytest -q uit in de .

Klik daarna op Check my work. Deel één herhaalt elke controle van les 1 tot en met 3, en controleert dat SERVICE_CONTRACT.md en README.md er zijn en wat de bestanden in tests/ importeren. Deel twee voert je tests uit met pytest, terwijl reading_client.requests niets kan versturen en reading_server.http geen kan binden: één keer tegen je code, waarbij elke test moet slagen, en dan één keer per functie, waarbij die functie wordt vervangen door een versie met één fout, in deze volgorde: handle dat een 308 stuurt waarvan de location het id weglaat; cached_show dat een verlopen item vraagt zonder If-None-Match; main dat 0 teruggeeft bij een foutstatus; call zonder timeout; handle dat een body meestuurt met zijn 304; create_book zonder location. Elke keer moet een test mislukken. Elke run stopt bij zijn eerste mislukking; een run die na 3 seconden nog bezig is, wordt gestopt, en na in totaal 8 seconden begint er geen nieuwe run meer. Hij leest je terminal niet.