Testing¶
Introduction¶
Arvel is built with testing in mind. Tests run the real app in-process over ASGI — no network, no separate server — and the framework ships fluent response assertions plus fakes for every external-facing service. Tests are async; use pytest with pytest-asyncio in auto mode.
Creating a Test App¶
There are two entry points: a context manager for quick, functional tests, and a base class for class-style suites.
Using create_test_app¶
create_test_app boots an Application, yields an httpx.AsyncClient wired to it over ASGI, and shuts down on exit — even if the test raises:
from arvel.testing import create_test_app
async def test_health() -> None:
async with create_test_app(app) as client:
response = await client.get("/health")
assert response.status_code == 200
Using ArvelTestCase¶
Subclass ArvelTestCase for class-style suites. Override providers to add the providers your test needs; asyncSetUp builds a minimal app (config + HTTP) and an AsyncClient, and asyncTearDown cleans both up:
from arvel.testing import ArvelTestCase
class TestPosts(ArvelTestCase):
providers = (MyServiceProvider,)
async def test_list(self) -> None:
response = await self.client.get("/posts")
assert response.status_code == 200
Testing HTTP Requests¶
The client is a standard httpx.AsyncClient, so all the usual verbs work:
Response Assertions¶
Wrap a response in TestResponse for fluent, chainable assertions:
from arvel.testing import TestResponse
result = TestResponse(await client.post("/posts", json={"title": "Hello"}))
result.assert_status(201).assert_json_path("data.title", "Hello")
Available assertions:
| Method | Checks |
|---|---|
assert_ok() | Status is 2xx |
assert_status(code) | Status equals code |
assert_unauthorized() | Status is 401 |
assert_forbidden() | Status is 403 |
assert_not_found() | Status is 404 |
assert_redirect(to=None) | Status is 3xx (and Location matches to) |
assert_json(expected) | Body equals expected exactly |
assert_json_path(path, value) | Dotted path resolves to value (supports list indices) |
assert_header(name, value=None) | Header present (and equals value) |
assert_cookie(name) | Cookie present |
Every assertion returns self, so they chain.
Authenticating as a User¶
ArvelTestCase.acting_as(user) authenticates subsequent requests as a user. It's strictly test-only and refuses to run unless the app environment is testing:
Faking Services¶
Every external-facing facade ships a fake that captures activity instead of performing it, with matching assertions. Cache and Storage come from arvel.facades; Mail and Event are submodule facades (WelcomeMail, OrderShipped, and ship_order are your own app code):
from arvel.facades import Cache, Storage
from arvel.facades.mail import Mail
from arvel.facades.event import Event
with Cache.fake():
await Cache.put("k", "v", ttl=60)
Cache.assert_stored("k")
with Mail.fake() as mailbox:
await Mail.to("[email protected]").send(WelcomeMail("Ada"))
assert len(mailbox.sent) == 1
with Storage.fake():
await Storage.disk().put("f.txt", b"...")
Storage.assert_exists("f.txt")
with Event.fake():
await ship_order(order)
Event.assert_dispatched(OrderShipped)
For broadcasting, use BroadcasterFake directly — it records each broadcast(...) call and exposes assert_broadcasted(...) (see Broadcasting). It's a driver-level fake, not a manager, so it isn't passed to Broadcast.set_manager(...).
Database Testing¶
ArvelTestCase.refresh_database() rolls back and re-applies migrations against the testing database between tests.
Warning
refresh_database() is opt-in and partial — it only acts when the app has bound a database connection and the framework exposes a refresh_database helper. Otherwise it's a safe no-op. Don't assume a clean database from it alone; manage test data explicitly until the helper is fully wired.