Skip to content

Mocking httpx Calls in Async Tests with respx

Code that calls HTTP APIs needs tests that do not depend on those APIs being up, fast or deterministic. respx replaces httpx's transport for the duration of a test, so requests are matched against routes you declare and answered with responses you construct — no network, no server, and the real httpx client code path above the transport. Tested with respx 0.23.1 and httpx 0.28, respx caught unmocked requests (AllMockedAssertionError), routes that were never called (assert_all_called), and replayed a connect error, a 503 and a 200 in sequence for retry tests. One behaviour surprised: a mock that slept 0.5 s did not trigger a client with a 0.1 s timeout — the request simply took 0.5 s, because httpx's timeouts are enforced inside the transport respx replaces. Timeout tests have to raise the timeout exception instead. This guide covers the patterns that make respx tests precise.

Prerequisites

  • Python 3.11+, pip install httpx respx pytest pytest-asyncio.
  • Async test setup, from Testing Async Code.
  • The code under test should accept an httpx.AsyncClient or create one inside; respx works with both.

1. Mock a route and assert it was called

Declare the request you expect and the response to return, run the code, then check the route:

import httpx
import respx


async def get_user(client: httpx.AsyncClient, uid: int) -> dict:
    r = await client.get(f"https://api.example.com/users/{uid}")
    r.raise_for_status()
    return r.json()


@respx.mock
async def test_get_user():
    route = respx.get("https://api.example.com/users/1").mock(
        return_value=httpx.Response(200, json={"id": 1})
    )
    async with httpx.AsyncClient() as client:
        assert await get_user(client, 1) == {"id": 1}
    assert route.call_count == 1

The @respx.mock decorator patches every httpx transport while the test runs — including clients created deep inside the code under test, which is what makes respx convenient for code that does not take a client as a parameter. By default, a request that matches no route fails the test: requesting /users/2 raised AllMockedAssertionError: RESPX: <Request('GET', 'https://api.example.com/users/2')> not mocked!. That default is valuable; it catches requests to URLs you did not expect.

Verify: change the URL in the code under test; the test fails with "not mocked" rather than reaching the network.

2. Scope mocks with a base URL and assert all routes were used

For tests of a client wrapper that calls several endpoints, use the context-manager form with a base_url, and turn on assert_all_called so a route the code never hit fails the test:

async def test_sync_flow():
    async with respx.mock(base_url="https://api.example.com", assert_all_called=True) as api:
        api.get("/users/1").respond(200, json={"id": 1, "team": 7})
        api.get("/teams/7").respond(200, json={"id": 7, "name": "core"})
        async with httpx.AsyncClient() as client:
            profile = await load_profile(client, 1)
    assert profile.team_name == "core"

Tested: with two routes declared and only one requested, leaving the block raised AssertionError: RESPX: some routes were not called!. That catches a whole class of silent regressions — a code change that stops fetching something, while the test still passes on cached or default data. respond(...) is shorthand for mock(return_value=httpx.Response(...)).

Verify: remove one of the calls from the code under test; the test fails on the unused route.

Where respx sits in an httpx request A flow of 4 stages. Where respx sits in an httpx request code under test client.get(...) httpx client build request, hooks respx transport match a route declared response or side effect Everything above the transport is real; only the network is replaced.

3. Simulate failures and retries with side effects

side_effect accepts a list that is consumed one item per call, where each item is a response or an exception. That is the natural way to test retry logic:

async def test_retries_until_success():
    async with respx.mock(base_url="https://api.example.com") as api:
        route = api.get("/users/1").mock(side_effect=[
            httpx.ConnectError("connection refused"),
            httpx.Response(503),
            httpx.Response(200, json={"id": 1}),
        ])
        async with httpx.AsyncClient() as client:
            user = await get_user_with_retry(client, 1)
    assert user == {"id": 1}
    assert route.call_count == 3

Tested: three calls returned ConnectError, then 503, then 200, in order. A list that runs out makes the next call fail — tested, it raised RuntimeError from the exhausted iterator — so a test also catches code that retries more than expected. The retry policy being tested here is the one from retrying httpx requests with transport retries.

Verify: the call count matches the number of attempts the retry policy should make, and an extra attempt fails the test.

4. Test timeouts by raising, not sleeping

The intuitive way to test a timeout is a slow mock. It does not work:

async def slow(request):
    await asyncio.sleep(0.5)
    return httpx.Response(200)


async def test_timeout_wrong():
    async with respx.mock() as m:
        m.get("https://api.example.com/slow").mock(side_effect=slow)
        async with httpx.AsyncClient(timeout=0.1) as client:
            await client.get("https://api.example.com/slow")   # measured: returned after 0.50 s, no timeout


async def test_timeout_right():
    async with respx.mock() as m:
        m.get("https://api.example.com/slow").mock(side_effect=httpx.ReadTimeout("timed out"))
        async with httpx.AsyncClient(timeout=0.1) as client:
            with pytest.raises(httpx.ReadTimeout):
                await client.get("https://api.example.com/slow")

httpx's connect, read, write and pool timeouts are enforced by its network layer, which respx bypasses, so a slow mock just takes long. Raise the specific timeout exception the network layer would raise — ConnectTimeout, ReadTimeout, PoolTimeout — to test the code that handles each. An overall deadline the application enforces with asyncio.timeout does fire on a slow mock, because that runs above the transport; see setting connect, read and total timeouts in async HTTP clients.

Verify: each timeout branch in the code has a test that raises the matching exception from a mock.

Which timeouts can be tested with a slow mock? A grid of 3 rows by 4 columns. Which timeouts can be tested with a slow mock? timeout enforced in slow mock fires it? how to test httpx connect/read/write transport no (0.5 s mock, 0.1 s limit) raise ConnectTimeout / ReadTimeout httpx pool connection pool no raise PoolTimeout asyncio.timeout in app code event loop yes slow side effect respx replaces the layer that enforces httpx's own timeouts.

5. Assert on what was sent

Responses test how code reacts; request assertions test what it sends. Match on content in the route and inspect the recorded call:

async def test_create_order_sends_auth_and_body():
    async with respx.mock() as m:
        route = m.post("https://api.example.com/orders", json__qty=2).respond(201, json={"id": "o1"})
        async with httpx.AsyncClient() as client:
            await create_order(client, sku="x", qty=2, token="t")
    request = route.calls.last.request
    assert request.headers["authorization"] == "Bearer t"
    assert json.loads(request.content) == {"sku": "x", "qty": 2}

json__qty=2 is a lookup pattern: the route only matches requests whose JSON body has qty equal to 2, so a request with the wrong body falls through to "not mocked" and fails. Patterns exist for params, headers, cookies and content. Recorded calls keep the full request, so authentication, idempotency keys and tracing headers can all be asserted.

Verify: a change to the request body or a missing header makes the test fail.

What should this HTTP test assert? A decision on What behaviour is under test with 4 outcomes. What should this HTTP test assert? What behaviour is under test? parsing a response respond() + result check call_count retries and errors side_effect list exact attempt count timeout handling raise ReadTimeout etc. not a slow mock what is sent lookup patterns calls.last.request Strict defaults — unmocked requests fail — keep tests honest.

Verification

HTTP tests with respx are sound when:

  • Unmocked requests fail the test, so nothing reaches the network.
  • assert_all_called catches calls the code stopped making.
  • Retries and timeouts are tested with exceptions in side-effect sequences.
  • Request contents are asserted, not just responses.

Diagnostic Hook: run the suite with networking disabled (for example pytest --disable-socket from pytest-socket). Any test that fails was reaching a real host — a missing route, or a client created with a custom transport that respx does not patch.

Pitfalls & edge cases

  • Testing timeouts with sleeps. httpx's timeouts do not fire on a slow mock.
  • Loose matching. Match on method, URL and body so wrong requests fail.
  • Custom transports. A client built with its own transport instance can bypass the patch; pass the mock router as the transport in that case.
  • Exhausted side-effect lists. An extra call raises, which is usually the bug you want to find.

Frequently Asked Questions

How do I mock httpx AsyncClient requests in pytest?

Use respx: decorate the test with @respx.mock or use async with respx.mock(), declare routes such as respx.get(url).respond(200, json=...), and run the code. Requests are answered by the routes without touching the network.

Why doesn't my httpx timeout fire in a respx test?

httpx enforces connect and read timeouts in the transport that respx replaces, so a slow mock just takes longer. Raise httpx.ReadTimeout or ConnectTimeout from the mock instead.

How do I test retries with respx?

Give the route a side_effect list of exceptions and responses, consumed one per call, and assert the route's call_count equals the expected number of attempts.

How do I check the request body sent to a mocked route?

Match it with lookups such as json__field=value, or read route.calls.last.request and assert on its headers and content.