8. Trial by Fire
HQ's lawyers ("Cape & Cowl LLP") won't let a single hero into the field until the backend is tested. They are, annoyingly, correct. Good news: Lauren ships an in-process test client, so you can put the whole agency through its paces without a real server or a real socket.
Abstract: 📋 Mission briefing
You'll build: a test suite that exercises routes, the guard, sessions, SSE, and comms. You'll learn:
- Driving the app with
TestClient— no network required- Testing guards, validation, and error envelopes
- Keeping session cookies in the client's jar and testing WebSockets with
WsTestClient
The in-process test client
TestClient runs your app through the ASGI protocol directly. It even runs startup for you,
so lifecycle hooks fire just like in production. Create one per app:
from lauren.testing import TestClient
from hero_hq.main import build_app
BADGE = {"X-HQ-Badge": "hq-badge-007"}
def client() -> TestClient:
return TestClient(build_app())
def test_recruit_then_fetch():
c = client()
r = c.post("/heroes/", json={"name": "Volt", "power": "lightning", "wattage": 9001}, headers=BADGE)
assert r.status_code == 201
hero = r.json()
assert c.get(f"/heroes/{hero['id']}").json()["name"] == "Volt"
def test_missing_hero_is_a_clean_404():
r = client().get("/heroes/999")
assert r.status_code == 404
assert r.json()["error"]["code"] == "hero_not_found"Each build_app() is a fresh, isolated app — no shared state leaks between tests.
Test the bouncer and the Auditor
A good suite proves the doors are locked and that the right people get in:
def test_recruiting_without_a_badge_is_403():
r = client().post("/heroes/", json={"name": "Sneaky", "power": "?", "wattage": 1})
assert r.status_code == 403
assert r.json()["error"]["code"] == "villain_detected"
def test_the_auditor_rejects_bad_paperwork():
r = client().post(
"/heroes/", json={"name": "Volt", "power": "lightning", "wattage": "lots"}, headers=BADGE
)
assert r.status_code == 422Tip: ⚡ Hero Tip
Test the unhappy paths first — the
403, the422, the404. Anyone can make the happy path work; the bugs that page you at 3 a.m. live in the rejections.
Test sessions: let the cookie jar do it
Used as a context manager, TestClient keeps a browser-like cookie jar: the session cookie
/me/login sets is replayed on the next request automatically — and with also runs the
app's shutdown hooks when the block ends.
def test_login_is_remembered():
with TestClient(build_app()) as c:
hero_id = c.post(
"/heroes/", json={"name": "Volt", "power": "lightning", "wattage": 9001}, headers=BADGE
).json()["id"]
c.post("/me/login", json={"hero_id": hero_id})
who = c.get("/me/")
assert who.json() == {"identified": True, "hero_id": hero_id, "name": "Volt"}Need a second hero in the same app? c.new_session() is another browser with an empty jar.
Test the live stuff
A finite SSE feed comes back whole from a plain get, so you can assert on the body
(client.stream(...) reads an endless one event by event). WebSockets get their own
WsTestClient:
import asyncio
from lauren.testing import WsTestClient
def test_status_feed_streams_then_closes():
body = client().get("/missions/feed").body
assert b"event: status" in body
assert b"event: close" in body
def test_comms_broadcasts_to_everyone():
app = build_app()
async def run():
ws_client = WsTestClient(app)
async with ws_client.connect("/comms") as a, ws_client.connect("/comms") as b:
await a.send_json({"event": "chat", "text": "assemble!"})
assert await a.receive_json() == {"chat": "assemble!"}
assert await b.receive_json() == {"chat": "assemble!"}
asyncio.run(run())Danger: 💥 Villainous Pitfall
Don't drive one app with two independent clients — an app's loop-bound resources live on one client's event loop. Use
client.new_session()for a second user, andbuild_app()per test to keep each trial hermetic. (HQ's lawyers love the word "hermetic.")
✅ Checkpoint
tests/
└── test_hero_hq.py # routes, guard, validation, sessions, SSE, commsRun it:
pytest -qWhat changed: every promise the API makes is now backed by a test — including the rejections. Cape & Cowl LLP is, grudgingly, satisfied.
Next: 9. Into Production → — the suite's green. Time to ship HQ
without the pager going off.
Go deeper: Testing skill ·
lauren.testing reference