Test Client

Make requests against your app without a running server

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

Overview

TestClient runs requests straight through your app's ASGI handler in-process -- no socket, no running server, no test HTTP layer. It builds the same Request and returns the same Responseyour app uses in production, so a passing test exercises the real code path.

Reach for it in pytest to assert status, body, and fragment-vs-full-page rendering. It also runs your startup and per-worker hooks, so apps that open a DB connection or HTTP client inon_worker_startupbehave exactly as they do in production.

Basic Usage

from chirp.testing import TestClient

async def test_homepage():
    async with TestClient(app) as client:
        response = await client.get("/")
        assert response.status == 200
        assert "Hello" in response.text

The TestClientis an async context manager. It handles app startup/shutdown lifecycle automatically.

HTTP Methods

Every method accepts aheaders= dict. post() also takes data= (form-encoded), json= (JSON body), or raw body= bytes; put() and delete() take only headers= and (for put) body=.

async def test_methods():
    async with TestClient(app) as client:
        # GET with custom headers
        response = await client.get("/api/data", headers={
            "Authorization": "Bearer token123",
            "Accept": "application/json",
        })
        assert response.status == 200

        # POST with JSON
        response = await client.post("/users", json={"name": "Alice"})
        assert response.status == 201

        # POST with form data
        response = await client.post("/login", data={"username": "alice", "password": "secret"})

        # PUT with a raw body (put() has no json= / data= shortcut)
        import json
        response = await client.put(
            "/users/1",
            body=json.dumps({"name": "Alice Updated"}).encode(),
            headers={"Content-Type": "application/json"},
        )

        # DELETE
        response = await client.delete("/users/1")
        assert response.status == 200

Fragment Requests

To simulate an htmx request, send theHX-Request header so your handler renders a fragment instead of a full page. The fragment() convenience method sets that header for you and exposes target=, trigger=, and history_restore=:

async def test_fragment():
    async with TestClient(app) as client:
        response = await client.fragment("/search?q=test", target="#results")
        assert response.status == 200
        assert '<div id="results">' in response.text

Use the fragment and SSE assertions (assert_is_fragment, assert_is_full_page, ...) to check fragment-vs-full-page rendering without hand-writing <html>string checks.

Cookies and sessions

Response Properties

The returned object is the sameResponseyour handlers produce. The fields you assert on most:

Property Type Description
status int HTTP status code
text str Response body as a string
json property Body parsed as JSON; raisesValueErroron non-JSON
header(name, default=None) method First matching header value (case-insensitive)
headers tuple[tuple[str, str], ...] Raw header pairs
cookies tuple[SetCookie, ...] Set-Cookievalues on the response

Read a single header with theheader() method rather than indexing headers:

assert response.header("Content-Type") == "application/json"

Using with pytest

import pytest
from myapp import app

@pytest.fixture
async def client():
    async with TestClient(app) as c:
        yield c

async def test_homepage(client):
    response = await client.get("/")
    assert response.status == 200