Skip to content

Testing guide

httpware's test seam is httpx2. Pass any httpx2.AsyncClient (including one built on httpx2.MockTransport) to AsyncClient(httpx2_client=...) — the middleware chain still runs end-to-end, only the wire is mocked. No special test mode, no monkey-patching, no respx.

The basic pattern

from http import HTTPStatus

import httpx2

from httpware import AsyncClient


def handler(request: httpx2.Request) -> httpx2.Response:
    return httpx2.Response(HTTPStatus.OK, json={"id": 1, "name": "Alice"})


async def test_get_user() -> None:
    transport = httpx2.MockTransport(handler)
    async with AsyncClient(httpx2_client=httpx2.AsyncClient(transport=transport)) as client:
        response = await client.get("https://api.example.test/users/1")
    assert response.status_code == HTTPStatus.OK
    assert response.json()["name"] == "Alice"

The handler can be sync or async; httpx2.MockTransport supports both. The test above uses a sync handler.

If you use pytest-asyncio in auto-mode (asyncio_mode = "auto" under [tool.pytest.ini_options]), async test functions don't need the @pytest.mark.asyncio decorator.

Sync Client

The same pattern works for the sync Client — pass an httpx2.Client (not httpx2.AsyncClient) built on httpx2.MockTransport:

from http import HTTPStatus

import httpx2

from httpware import Client


def test_get_returns_typed_response() -> None:
    def handler(request: httpx2.Request) -> httpx2.Response:
        return httpx2.Response(HTTPStatus.OK, request=request, json={"ok": True})

    with Client(httpx2_client=httpx2.Client(transport=httpx2.MockTransport(handler))) as client:
        response = client.get("https://example.test/x")

    assert response.status_code == HTTPStatus.OK
    assert response.json() == {"ok": True}

Recording / stateful handlers

For tests that need to vary the response by call count or assert on the requests that came in, use a handler with instance state:

from httpware import AsyncRetry


class _ResponseSequence:
    """Returns each status in order; records every request received."""

    def __init__(self, statuses: list[int]) -> None:
        self._statuses = list(statuses)
        self.calls: list[httpx2.Request] = []

    def __call__(self, request: httpx2.Request) -> httpx2.Response:
        self.calls.append(request)
        status = self._statuses.pop(0) if self._statuses else HTTPStatus.OK
        return httpx2.Response(status, request=request)


async def test_retry_succeeds_after_503() -> None:
    handler = _ResponseSequence([HTTPStatus.SERVICE_UNAVAILABLE, HTTPStatus.OK])
    transport = httpx2.MockTransport(handler)
    async with AsyncClient(
        httpx2_client=httpx2.AsyncClient(transport=transport),
        middleware=[AsyncRetry(base_delay=0.001, max_delay=0.002)],
    ) as client:
        response = await client.get("https://example.test/x")
    assert response.status_code == HTTPStatus.OK
    assert len(handler.calls) == 2  # initial + 1 retry

The base_delay/max_delay are set tiny so the test runs instantly — no need for freezegun or sleep injection in most cases.

Testing your custom middleware

Compose your middleware with the mock transport to exercise the chain end-to-end:

async def test_my_middleware_adds_header() -> None:
    handler = _ResponseSequence([HTTPStatus.OK])
    async with AsyncClient(
        httpx2_client=httpx2.AsyncClient(transport=httpx2.MockTransport(handler)),
        middleware=[MyHeaderMiddleware()],
    ) as client:
        await client.get("https://example.test/x")
    assert handler.calls[0].headers["X-My-Header"] == "expected-value"

For middleware with state-keeping (counters, circuit-breaker state), assert on instance attributes after running the call.

Why not respx?

httpware deliberately uses httpx2.MockTransport instead of respx for its own tests. MockTransport is the public test seam in httpx — supported by the maintainers, stable across versions, lives in the public API surface. respx patches private internals and has historically broken across httpx major versions. Stick with MockTransport unless you have a specific reason not to.

See also

  • Middleware guide — write the middleware you're testing.
  • Resilience reference — testing AsyncRetry/AsyncBulkhead configurations.
  • planning/engineering.md §6 — the project's own testing patterns (Hypothesis property-based tests, pytest-asyncio auto-mode, the RecordedTransport-was-removed history).