Testing
In-process ASGI test clients for unit and integration tests.
TestClient
class TestClient(app: Any, base_url: str = 'http://testserver', headers: Mapping[str, str] | None = None, cookies: Mapping[str, str] | None = None, follow_redirects: bool = False, timeout: float | None = _UNSET, raise_server_exceptions: bool = False, lifespan: LifespanMode = 'auto', backend: Literal['asyncio'] = 'asyncio', backend_options: Mapping[str, Any] | None = None, root_path: str = '', strict_cookies: bool = False, persist_cookies: bool | None = None)Synchronous in-process client for a LaurenApp, a Lauren app or any ASGI app.
One persistent event loop (a daemon "portal" thread) per client runs the app's startup, every request, background tasks, WebSockets and shutdown, so loop-bound resources created at startup keep working.
with TestClient(app) as client:runsstartup()on enter andshutdown()(drain →on_shutdown→@pre_destruct→ generator teardown) on exit; a failing hook raises its original exception.TestClient(app)withoutwithkeeps the 1.x behaviour: aLaurenAppis started right away, aLaurenapp on its first request.close()shuts it down; a client that is never closed stops its loop when garbage-collected (without shutdown hooks).- Cookies in
cookiesare sent with every matching request.Set-Cookieresponses are stored there when the client is used as a context manager or comes fromnew_session; a bareTestClient(app)keeps the 1.x behaviour of not storing them unlesspersist_cookies=True(2.0 stores them for every client).
Creating a second client for an app that a live client already drives
attaches it to that client's loop (it shares the lifecycle, like
new_session); entering such a client with with raises.
TestClient.close
def close(self) -> NoneShut the app down (owning client only) and stop the event loop.
Runs drain → on_shutdown → @pre_destruct → generator teardown
(unless lifespan="off"), then cancels tasks the app left running
— with a RuntimeWarning naming them — and stops the portal
thread. A client from new_session only detaches.
TestClient.new_session
def new_session(self, headers: Mapping[str, str] | None = None, cookies: Mapping[str, str] | None = None) -> 'TestClient'A client for the same app and loop with its own (empty) cookie jar and default headers.
The new client shares this client's lifecycle: this client keeps ownership of startup and shutdown.
TestClient.request
def request(self, method: str, url: str, params: Any = None, headers: Any = None, cookies: Mapping[str, str] | None = None, json: Any = None, data: Any = None, files: Any = None, content: Any = None, follow_redirects: bool | None = None, timeout: float | None = _UNSET, disconnect_after: float | None = None) -> TestResponseSend one request and return the buffered TestResponse.
TestClient.arequest
def arequest(self, method: str, url: str, kwargs: Any = {}) -> TestResponseawait-able request: runs on the client's loop without blocking the caller's.
TestClient.get
def get(self, url: str, kw: Any = {}) -> TestResponseTestClient.post
def post(self, url: str, kw: Any = {}) -> TestResponseTestClient.put
def put(self, url: str, kw: Any = {}) -> TestResponseTestClient.patch
def patch(self, url: str, kw: Any = {}) -> TestResponseTestClient.delete
def delete(self, url: str, kw: Any = {}) -> TestResponseTestClient.head
def head(self, url: str, kw: Any = {}) -> TestResponseTestClient.options
def options(self, url: str, kw: Any = {}) -> TestResponseTestClient.stream
def stream(self, method: str, url: str, params: Any = None, headers: Any = None, cookies: Mapping[str, str] | None = None, json: Any = None, data: Any = None, files: Any = None, content: Any = None, timeout: float | None = _UNSET, disconnect_after: float | None = None) -> Iterator[StreamingTestResponse]Send a request and read the response incrementally.
timeout bounds the wait for the response start and for each body
chunk, so an endless stream cannot hang the test. Leaving the block
(or response.close()) delivers http.disconnect to the app if
the response is still in flight. Redirects are not followed.
TestClient.websocket_connect
def websocket_connect(self, path: str, headers: Any = None, subprotocols: Iterable[str] | None = None, query_string: str = '', timeout: float | None = 5.0) -> Iterator[WebSocketSession]Open a WebSocket on the client's loop; the jar's cookies ride along.
timeout bounds every receive and the server's wind-down after the
block exits.
TestClient.resolve
def resolve(self, token: Any) -> AnyResolve token from the app's DI container, on the client's loop.
TestClient.capture
def capture(self, events: type = ()) -> Iterator[list[Any]]Collect the lifecycle events (all of them when none are named) emitted inside the block.
TestClient.set_session
def set_session(self, data: Mapping[str, Any]) -> NoneStore a valid signed session cookie holding data, issued by the app's session engine.
WsTestClient
class WsTestClient(app: Any)Factory for WebSocketTestSession bound to an ASGI app.
Mirrors the ergonomic pattern of TestClient — one client
instance per app, one WebSocketTestSession per connection.
Use as:
client = WsTestClient(app)
async with client.connect("/chat/42", headers={...}) as ws:
...WsTestClient.connect
def connect(self, path: str, headers: Mapping[str, str] | Iterable[tuple[str, str]] | None = None, subprotocols: Iterable[str] | None = None, query_string: str = '', server_timeout: float = 2.0) -> WebSocketTestSessionOpen a session; server_timeout bounds the server's wind-down on exit.