Testing with MockTransport

HTTPX · Practical · 11 min

Built-in transports make HTTP code testable without patching or extra libraries.

Every HTTPX client sends requests through a transport. Normally that is HTTPTransport, which opens sockets. httpx.MockTransport(handler) replaces it with a plain function: the handler receives an httpx.Request and returns an httpx.Response. No network is touched.

def handler(request):
    return httpx.Response(200, json={"ok": True})

client = httpx.Client(transport=httpx.MockTransport(handler))

Because the handler is ordinary Python, it can route on request.method and request.url.path, inspect headers and bodies (request.content, json.loads(request.content)), simulate errors by returning 500s, or raise httpx.ConnectError / httpx.ReadTimeout to test failure handling. It can also record calls in a list so the test can assert what was sent.

The same transport works with AsyncClient (the handler may be sync or async).

To make code testable, let it accept a client (dependency injection) rather than creating one internally. Production code passes a real client; tests pass one with a MockTransport. This is cleaner than monkeypatching.

Third-party helpers build on this: respx (route-based mocking for HTTPX) and pytest-httpx (a fixture). They are convenient for large test suites, but MockTransport alone covers most needs.

Example

import httpx

calls = []

def handler(request: httpx.Request) -> httpx.Response:
    calls.append((request.method, request.url.path))
    if request.url.path == "/users/1":
        return httpx.Response(200, json={"id": 1, "name": "Ada"})
    if request.url.path == "/flaky":
        raise httpx.ConnectError("boom", request=request)
    return httpx.Response(404, json={"error": "not found"})

def get_name(client, user_id):
    r = client.get(f"/users/{user_id}")
    return r.json()["name"] if r.is_success else None

client = httpx.Client(base_url="https://api.test", transport=httpx.MockTransport(handler))
print(get_name(client, 1), get_name(client, 2))   # Ada None
try:
    client.get("/flaky")
except httpx.ConnectError as e:
    print("ConnectError:", e)                     # ConnectError: boom
print(calls)  # [('GET', '/users/1'), ('GET', '/users/2'), ('GET', '/flaky')]

Remember

  • MockTransport(handler) turns a function into the network
  • Handlers can route, inspect requests, return errors or raise exceptions
  • Accept a client as a parameter so tests can inject a mocked one

Watch out for

  • Creating the client inside the function under test, so there is no way to inject a mock.

Check yourself

  1. What does a MockTransport handler receive and return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.python-httpx.org/advanced/transports/#mock-transports