Testing¶
jero ships a synchronous, in-process TestClient — it drives the ASGI app directly,
no socket and no running server. It runs the app's full lifespan (so wire registers
routes and the dependency context stays open) on a background event loop, and exposes a
requests-style API.
# doc-example: fragment (illustrative snippet, not a runnable app)
from jero.testing import TestClient
def test_read_one():
with TestClient(App()) as client:
resp = client.get("/widgets/abc")
assert resp.status_code == 200
assert resp.json() == {"id": "abc", "name": "widget-name"}
Use it as a context manager (or call close()) for deterministic shutdown — that's
what runs the app's resource teardown.
The request API¶
get / post / put / patch / delete / head / options, plus request. Bodies
go in as json=, content= (raw bytes), or data= / files= (multipart). content=
also takes an iterable of bytes, sent as one ASGI message per item, to exercise a
multi-chunk body (a content_stream
handler's re-framing, say):
client.post("/widgets", json={"name": "n", "priceCents": 100}, headers={"authorization": "Bearer token"})
client.post("/upload", files={"document": ("report.pdf", b"...", "application/pdf")})
client.post("/uploads", content=[b"first-chunk", b"second-chunk"])
client.get("/widgets", params={"limit": "5"})
client.get("/theme", cookies={"session_id": "session-value"})
TestClient(App(), cookie_jar=True) opts into automatic cookie persistence across
requests (and into WebSocket handshakes) — off by default. See
Cookies for the full request/response/jar treatment.
Each call returns a TestResponse with status_code, headers, content, .text,
.json(), .cookies (parsed Set-Cookie headers, by name — see Cookies),
and multi_headers — the faithful wire pair list, repeats included, for anything
.cookies/.headers doesn't cover.
Streaming¶
For streaming endpoints, use the stream_* methods. They return a session you iterate
for decoded chunks (and that you can use as a context manager to disconnect early):
with TestClient(App()) as client:
# NDJSON -> decoded objects
assert list(client.stream_get("/movies")) == [{"title": "a"}, {"title": "b"}]
# SSE -> decoded events
with client.stream_get("/events") as events:
first = next(events)
assert first.data == {"title": "a"}
assert first.event == "added"
# leaving the block disconnects; lifecycle teardown runs
Undeclared errors¶
When an operation declares its errors with exceptions=[...], every TestClient
request checks the answer against that list: an error the OpenAPI spec doesn't document
fails the request with an AssertionError naming the operation and the error class.
There is nothing to turn on. See
Keeping the list complete for exactly what
counts as documented.
Mocking dependencies¶
Inject a stand-in factory through the factory= seam so the real I/O services are
never built:
from unittest.mock import create_autospec
def test_create_widget(mocker):
factory_mock = create_autospec(Factory, spec_set=True, instance=True)
widget_service_mock = create_autospec(WidgetService, spec_set=True, instance=True)
factory_mock.create_widget_service.return_value = widget_service_mock
widget_service_mock.create_widget.return_value = Widget(id="1", name="n")
with TestClient(App(factory=factory_mock)) as client:
resp = client.post("/widgets", json={...}, headers={...})
assert resp.status_code == 201
widget_service_mock.create_widget.assert_awaited_once()
FactoryHarness¶
To test the real factory wiring that factory= mocks away — that each create_*
builds the right service and opens/closes its resources — use FactoryHarness. It is
the sync-test bridge over Factory.open() (the
standalone entry point): the same lifecycle, entered on a
background loop so synchronous test code can drive async create_* methods, matching
the sync TestClient. Outside tests, use async with Factory.open() directly.
# doc-example: fragment (illustrative snippet, not a runnable app)
from jero.testing import FactoryHarness
def test_factory_builds_service():
with FactoryHarness(Factory) as harness:
service = harness.run(harness.factory.create_widget_service())
assert isinstance(service, WidgetService)
# everything opened on the stacks is closed here