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.