One Server, Many Clients · practice
Test With Fake Connections
Join, registry and broadcast work, and so far only the demo and Submit have shown it. This lesson has you write tests of your own in tests/test_scoreboard.py, and run them in the
A stand-in, read from the demo
Open scoreboard_demo.py and find demo_connection. It builds a stand-in from three recv, send and close, which share one state. send parses each message and appends it to state["sent"], or raises ConnectionClosedError when the stand-in was made with fail_send=True. The three functions are then collected, with an id and a request, into a types.SimpleNamespace, an connection.send(...) and connection.id on it exactly as it would on a library connection, and never knows the difference. Python in Practice built stand-in sessions the same way.
The tests need less than that. add_viewer, apply_score and broadcast only ever use id and send, so make_connection(conn_id, *, fail_send=False) can be small:
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() is supplied at the top of the file: it builds the ConnectionClosedError a send raises when the viewer’s connection ended without a close sent on the stand-in lets a test read viewer.sent afterwards.
Four tests
Each test follows the same three steps: arrange a board and some stand-ins, act through one call to the server, and assert what the stand-ins recorded and who is in board["viewers"].
test_a_viewer_gets_the_board_when_it_joins: make a board, change its version and one team’s points, calladd_viewer, and assert that the stand-in recorded exactly that snapshot and that its id is in the registry.test_a_score_reaches_every_viewer: add two viewers, apply one score, and assert that both received the new version and points.test_a_failed_viewer_is_dropped_and_the_rest_still_hear: register a viewer made withfail_send=Truebefore a working one, apply a score, and assert that the failed one has left the registry and that the working one still received the update. The order matters: a failed viewer registered last could not show whether thewent on after it. test_parse_message_refuses: keep the@pytest.mark.parametrizeline, the one you met in Chapter 3, and give it at least three texts thatparse_messagemust refuse. Thetruepoints case is already there. Inside, callparse_messagein atry; return fromexcept ValueError:, and after thetrystatement writeassert False, ...with the text, so a text that is accepted fails the test.
To put a stand-in in the registry without a join snapshot, board["viewers"]["broken"] = broken.
Which version of the server does test_a_failed_viewer_is_dropped_and_the_rest_still_hear exist to catch?
Why through the module
The file imports the import scoreboard_server and import scoreboard_messages, and the tests call scoreboard_server.apply_score(...). Keep it that way. Check my work runs your tests against versions of the server that each have one mistake, swapping the function in the module while your tests run, to see whether your tests notice. A call through the module looks the function up at the moment of the call, so it gets the swapped one. A name imported with from scoreboard_server import apply_score was bound once, when the file was imported, and keeps the old function. For the same reason, make every call inside a test function, not at the top of the file.
Why do the tests call scoreboard_server.apply_score(...) instead of importing apply_score with from?
Run them
This lesson uses the terminal. Run the tests from the project folder:
python -m pytest -q
Each placeholder fails until you replace it. On the site, the finished file reported 8 passed: three tests, and five texts in the parametrized one, each counted as a test of its own. The demo still runs as before, and its output has not changed since Lesson 4:
python scoreboard_demo.py
These tests prove what one thread sees: the join snapshot, the broadcast and the removal of a gone viewer. They do not prove the race at the door from Lesson 3, which needs two threads at exactly the wrong moment. That one was proved by an instrument of the check’s own when you pressed Submit in Lesson 3; your tests do not have to.
When every test passes, press Check my work. The next lesson writes the scorekeeper’s handler, the last piece of scoreboard v1, and runs it with several viewers on
Task
In tests/test_scoreboard.py, write make_connection and replace the four placeholders with tests. Keep closed_error and the imports as they are.
make_connection(conn_id, *, fail_send=False): return atypes.SimpleNamespacewithid, asendthat appendsjson.loads(text)to a(or raises closed_error()whenfail_sendis true), and that list assent.test_a_viewer_gets_the_board_when_it_joins: afterscoreboard_server.add_viewer, the stand-in recorded the board’s snapshot, with its version and scores, and its id is inboard["viewers"].test_a_score_reaches_every_viewer: two viewers, onescoreboard_server.apply_score, and both received the new snapshot.test_a_failed_viewer_is_dropped_and_the_rest_still_hear: a failing viewer registered before a working one; after a score, the failing one is gone fromboard["viewers"]and the working one received the update.test_parse_message_refuses: parametrized with at least three texts, thetruepoints case among them, each of whichscoreboard_messages.parse_messagemust refuse with.
Call every server scoreboard_server. and scoreboard_messages. inside the tests, never with from ... import. Use stand-ins only: no
Run python -m pytest -q in the add_viewer that registers without sending the snapshot, a broadcast that stops at the first viewer that has gone, a broadcast that keeps that viewer, and an is_whole_number that accepts True. Against each, at least one test must fail. A run against the working scoreboard that takes longer than 1 second is stopped. The check does not read your terminal.