0%

Eén server, veel clients · oefening

Test met nepverbindingen

Binnenkomen, register en broadcast werken, en tot nu toe hebben alleen de demo en Submit dat laten zien. In deze les schrijf je eigen tests in tests/test_scoreboard.py en draai je ze in de . De berichtentest blijft de rest van de cursus staan; de drie servertests beschrijven versie één van het scorebord, en hoofdstuk 8 vervangt ze. Geen ervan opent een of start een thread: elke test geeft de server een stand-in-verbinding en controleert wat de stand-in vastlegde.

Een stand-in, afgekeken van de demo

Open scoreboard_demo.py en zoek demo_connection. Die bouwt een stand-in uit drie die erin zijn gedefinieerd, recv, send en close, die één met de naam state delen. send parset elk bericht en voegt het toe aan state["sent"], of werpt ConnectionClosedError op als de stand-in is gemaakt met fail_send=True. De drie functies worden daarna, met een id en een request, verzameld in een types.SimpleNamespace, een met als attributen wat je het meegeeft. De server roept er connection.send(...) en connection.id op aan, precies zoals hij dat bij een verbinding van de bibliotheek zou doen, en merkt het verschil nooit. In Python in Practice bouwde je stand-in-sessies op dezelfde manier.

De tests hebben minder nodig. add_viewer, apply_score en broadcast gebruiken alleen id en send, dus make_connection(conn_id, *, fail_send=False) kan klein zijn:

def make_connection(conn_id, *, fail_send=False):
    sent = []

    def send(text):
        if fail_send:
            raise closed_error()
        sent.append(json.loads(text))

    return types.SimpleNamespace(id=conn_id, send=send, sent=sent)

closed_error() staat meegeleverd bovenaan het bestand: hij bouwt de ConnectionClosedError die een send opwerpt als de verbinding van de kijker eindigde zonder close-. Door sent op de stand-in te zetten, kan een test achteraf viewer.sent lezen.

Vier tests

Elke test volgt dezelfde drie stappen: zet een bord en een paar stand-ins klaar, handel via één aanroep van de server, en controleer wat de stand-ins vastlegden en wie er in board["viewers"] staat.

  • test_a_viewer_gets_the_board_when_it_joins: maak een bord, verander de versie en de punten van één team, roep add_viewer aan, en controleer dat de stand-in precies die snapshot vastlegde en dat zijn id in het register staat.

  • test_a_score_reaches_every_viewer: voeg twee kijkers toe, verwerk één score, en controleer dat beide de nieuwe versie en punten ontvingen.

  • test_a_failed_viewer_is_dropped_and_the_rest_still_hear: registreer een kijker die is gemaakt met fail_send=True vóór een werkende, verwerk een score, en controleer dat de mislukte het register heeft verlaten en dat de werkende de update toch ontving. De volgorde doet ertoe: een mislukte kijker die als laatste geregistreerd is, kan niet laten zien of de na hem doorging.

  • test_parse_message_refuses: houd de regel @pytest.mark.parametrize, die je in hoofdstuk 3 tegenkwam, en geef hem minstens drie teksten die parse_message moet weigeren. Het geval met true als punten staat er al. Roep binnenin parse_message aan in een try; keer terug vanuit except ValueError:, en schrijf na het try-statement assert False, ... met de tekst, zodat een tekst die wordt geaccepteerd de test laat falen.

Om een stand-in zonder snapshot bij binnenkomst in het register te zetten, wijs je hem direct toe: board["viewers"]["broken"] = broken.

Welke versie van de server moet test_a_failed_viewer_is_dropped_and_the_rest_still_hear betrappen?

Waarom via de module

Het bestand importeert de , import scoreboard_server en import scoreboard_messages, en de tests roepen scoreboard_server.apply_score(...) aan. Houd dat zo. Check my work draait je tests tegen versies van de server die elk één fout bevatten, door de functie in de module te verwisselen terwijl je tests draaien, om te zien of je tests het merken. Een aanroep via de module zoekt de functie op op het moment van de aanroep, dus die krijgt de verwisselde. Een naam die met from scoreboard_server import apply_score is geïmporteerd, werd één keer gebonden, toen het bestand werd geïmporteerd, en houdt de oude functie. Om dezelfde reden doe je elke aanroep binnen een testfunctie, niet bovenaan het bestand.

Waarom roepen de tests scoreboard_server.apply_score(...) aan in plaats van apply_score met from te importeren?

Draai ze

Deze les gebruikt de terminal. Draai de tests vanuit de projectmap:

python -m pytest -q

Elke placeholder faalt totdat je hem vervangt. Op de site meldde het afgemaakte bestand 8 passed: drie tests, en vijf teksten in de geparametriseerde, die elk als aparte test worden geteld. De demo draait nog zoals eerst, en zijn uitvoer is sinds les 4 niet veranderd:

python scoreboard_demo.py

Deze tests bewijzen wat één thread ziet: de snapshot bij binnenkomst, de broadcast en het verwijderen van een verdwenen kijker. Ze bewijzen niet de race bij de deur uit les 3, want die heeft twee threads op precies het verkeerde moment nodig. Die werd bewezen door een eigen instrument van de controle toen je in les 3 op Submit klikte; je tests hoeven dat niet.

Als elke test slaagt, klik je op Check my work. De volgende les schrijft de handler van de scorebijhouder, het laatste stuk van scorebord v1, en draait hem met meerdere kijkers op .

Opdracht

Schrijf in tests/test_scoreboard.py make_connection en vervang de vier placeholders door tests. Laat closed_error en de imports zoals ze zijn.

  • make_connection(conn_id, *, fail_send=False): geef een types.SimpleNamespace terug met id, een send die json.loads(text) aan een toevoegt (of closed_error() opwerpt als fail_send waar is), en die lijst als sent.

  • test_a_viewer_gets_the_board_when_it_joins: na scoreboard_server.add_viewer heeft de stand-in de snapshot van het bord vastgelegd, met zijn versie en scores, en staat zijn id in board["viewers"].

  • test_a_score_reaches_every_viewer: twee kijkers, één scoreboard_server.apply_score, en beide ontvingen de nieuwe snapshot.

  • test_a_failed_viewer_is_dropped_and_the_rest_still_hear: een falende kijker, geregistreerd vóór een werkende; na een score is de falende weg uit board["viewers"] en heeft de werkende de update ontvangen.

  • test_parse_message_refuses: geparametriseerd met minstens drie teksten, waaronder het geval met true als punten, die scoreboard_messages.parse_message elk met moet weigeren.

Roep elke serverfunctie binnen de tests aan via scoreboard_server. en scoreboard_messages., nooit met from ... import. Gebruik alleen stand-ins: geen , geen thread, geen wachten.

Draai python -m pytest -q in de totdat elke test slaagt, en klik daarna op Check my work. Die leest je testbestand en draait je tests daarna in dit proces: één keer met een eigen werkend scorebord, waarbij minstens vier tests moeten worden verzameld (één geparametriseerd met minstens drie teksten) en ze allemaal moeten slagen, en één keer met elk van vier scoreborden die elk één fout bevatten: een add_viewer die registreert zonder de snapshot te sturen, een broadcast die stopt bij de eerste kijker die weg is, een broadcast die die kijker houdt, en een is_whole_number die True accepteert. Tegen elk ervan moet minstens één test falen. Een run tegen het werkende scorebord die langer dan 1 seconde duurt, wordt gestopt. De controle leest je terminal niet.