User Guide
RESPX is a mock router, capturing requests sent by HTTPX
, mocking their responses.
Inspired by the flexible query API of the Django ORM, requests are filtered and matched against routes and their request patterns and lookups.
Request patterns are bits of the request, like host
method
path
etc,
with given lookup values, combined using bitwise operators to form a Route
,
i.e. respx.route(path__regex=...)
A captured request, matching a Route
, resolves to a mocked httpx.Response
, or triggers a given side effect.
To skip mocking a specific request, a route can be marked to pass through.
Mock HTTPX
To patch HTTPX
, and activate the RESPX router,
use the respx.mock
decorator/context manager, or the respx_mock
pytest fixture.
Using the Decorator
import httpx
import respx
@respx.mock
def test_decorator():
my_route = respx.get("https://example.org/")
response = httpx.get("https://example.org/")
assert my_route.called
assert response.status_code == 200
Using the Context Manager
import httpx
import respx
def test_ctx_manager():
with respx.mock:
my_route = respx.get("https://example.org/")
response = httpx.get("https://example.org/")
assert my_route.called
assert response.status_code == 200
Using the pytest Fixture
import httpx
def test_fixture(respx_mock):
my_route = respx_mock.get("https://example.org/")
response = httpx.get("https://example.org/")
assert my_route.called
assert response.status_code == 200
Router Settings
The RESPX router can be configured with built-in assertion checks and an optional base URL.
By configuring, an isolated router is created, and settings are locally bound to the routes added.
Either of the decorator, context manager and fixture takes the same configuration arguments.
See router configuration reference for more details.
Configure the Decorator
When decorating a test case with configured router settings, the test function will receive the router instance as a respx_mock
argument.
@respx.mock(...)
def test_something(respx_mock):
...
Configure the Context Manager
When passing settings to the context manager, the configured router instance will be yielded.
with respx.mock(...) as respx_mock:
...
Configure the Fixture
To configure the router when using the pytest
fixture, decorate the test case with the respx
pytest marker.
@pytest.mark.respx(...)
def test_something(respx_mock):
...
Base URL
When adding a lot of routes, sharing the same domain/prefix, you can configure the router with a base_url
to be used for added routes.
import httpx
import respx
from httpx import Response
@respx.mock(base_url="https://example.org/api/")
async def test_something(respx_mock):
async with httpx.AsyncClient(base_url="https://example.org/api/") as client:
respx_mock.get("/baz/").mock(return_value=Response(200, text="Baz"))
response = await client.get("/baz/")
assert response.text == "Baz"
Assert all Mocked
By default, asserts that all sent and captured HTTPX
requests are routed and mocked.
@respx.mock(assert_all_mocked=True)
def test_something(respx_mock):
response = httpx.get("https://example.org/") # Not mocked, will raise
If disabled, all non-routed requests will be auto-mocked with status code 200
.
@respx.mock(assert_all_mocked=False)
def test_something(respx_mock):
response = httpx.get("https://example.org/") # Will auto-mock
assert response.status_code == 200
Assert all Called
By default, asserts that all added and mocked routes were called when exiting decorated test case, context manager scope or exiting a text case using the pytest fixture.
@respx.mock(assert_all_called=True)
def test_something(respx_mock):
respx_mock.get("https://example.org/")
respx_mock.get("https://some.url/") # Not called, will fail the test
response = httpx.get("https://example.org/")
@respx.mock(assert_all_called=False)
def test_something(respx_mock):
respx_mock.get("https://example.org/")
respx_mock.get("https://some.url/") # Not called, yet not asserted
response = httpx.get("https://example.org/")
assert response.status_code == 200
Routing Requests
The easiest way to add routes is to use the HTTP Method helpers.
For full control over the request pattern matching, use the route API.
Routes are matched and routed in added order. This means that routes with more specific patterns should to be added earlier than the ones with less "details".
HTTP Method Helpers
Each HTTP method has a helper function (get
, options
, head
, post
, put
, patch
, delete
), shortcutting the route API.
my_route = respx.get("https://example.org/", params={"foo": "bar"})
response = httpx.get("https://example.org/", params={"foo": "bar"})
assert my_route.called
assert response.status_code == 200
See .get(), .post(), ... helpers reference for more details.
Route API
Patterns
With the route
API, you define a combined pattern to match, capturing a sent request.
my_route = respx.route(method="GET", host="example.org", path="/foobar/")
response = httpx.get("https://example.org/foobar/")
assert my_route.called
assert response.status_code == 200
See .route() reference for more details.
Lookups
Each pattern has a default lookup. To specify what lookup to use, add a __<lookup>
suffix.
respx.route(method__in=["PUT", "PATCH"])
Combining Patterns
For even more flexibility, you can define combined patterns using the M() object, together with bitwise operators (&
, |,
~
), creating a reusable pattern.
hosts_pattern = M(host="example.org") | M(host="example.com")
my_route = respx.route(hosts_pattern, method="GET", path="/foo/")
response = httpx.get("http://example.org/foo/")
assert response.status_code == 200
assert my_route.called
response = httpx.get("https://example.com/foo/")
assert response.status_code == 200
assert my_route.call_count == 2
NOTE
M(url="//example.org/foobar/")
is equal to M(host="example.org") & M(path="/foobar/")
Named Routes
Routes can be named when added, and later accessed through the respx.routes
mapping.
This is useful when a route is added outside the test case, e.g. access or assert route calls.
import httpx
import respx
# Added somewhere else
respx.get("https://example.org/", name="home")
@respx.mock
def test_route_call():
httpx.get("https://example.org/")
assert respx.routes["home"].called
assert respx.routes["home"].call_count == 1
last_home_response = respx.routes["home"].calls.last.response
assert last_home_response.status_code == 200
Reusable Routers
As described under settings, an isolated router is created when calling respx.mock(...)
.
Isolated routers are useful when mocking multiple remote APIs, allowing grouped routes per API, and to be mocked individually or stacked for reuse across tests.
Use the router instance as decorator or context manager to patch HTTPX
and activate the routes.
import httpx
import respx
api_mock = respx.mock(base_url="https://api.foo.bar/", assert_all_called=False)
api_mock.get("/baz/", name="baz").mock(
return_value=httpx.Response(200, json={"name": "baz"}),
)
...
@api_mock
def test_decorator():
response = httpx.get("https://api.foo.bar/baz/")
assert response.status_code == 200
assert response.json() == {"name": "baz"}
assert api_mock["baz"].called
def test_ctx_manager():
with api_mock:
...
Catch-all
Add a catch-all route last as a fallback for any non-matching request, e.g. api_mock.route().respond(404)
.
NOTE
Named routes in a reusable router can be directly accessed via my_mock_router[<route name>]
Route with an App
As an alternative one can route and mock responses with an app
by passing either a respx.WSGIHandler
or respx.ASGIHandler
as side effect when mocking.
Sync App Example
import httpx
import respx
from flask import Flask
app = Flask("foobar")
@app.route("/baz/")
def baz():
return {"ham": "spam"}
@respx.mock(base_url="https://foo.bar/")
def test_baz(respx_mock):
app_route = respx_mock.route().mock(side_effect=WSGIHandler(app))
response = httpx.get("https://foo.bar/baz/")
assert response.json() == {"ham": "spam"}
assert app_route.called
Async App Example
import httpx
import respx
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
async def baz(request):
return JSONResponse({"ham": "spam"})
app = Starlette(routes=[Route("/baz/", baz)])
@respx.mock(base_url="https://foo.bar/")
async def test_baz(respx_mock):
app_route = respx_mock.route().mock(side_effect=ASGIHandler(app))
response = await httpx.AsyncClient().get("https://foo.bar/baz/")
assert response.json() == {"ham": "spam"}
assert app_route.called
Mocking Responses
To mock a route response, use <route>.mock(...)
to either...
- set the
httpx.Response
to be returned. - set a side effect to be triggered.
The route's mock interface is inspired by pythons built-in Mock()
object,
e.g. side_effect
has precedence over return_value
, side effects can either be functions, exceptions or an iterable, raising StopIteration
when "exhausted" etc.
Mock a Response
Create a mocked HTTPX
Response object and pass it as return_value
.
respx.get("https://example.org/").mock(return_value=Response(204))
See .mock() reference for more details.
You can also use the <route>.return_value
setter.
route = respx.get("https://example.org/")
route.return_value = Response(200, json={"foo": "bar"})
Mock with a Side Effect
RESPX side effects works just like the python Mock
side effects.
It can either be a function to call, an exception to raise, or an iterable of responses/exceptions to respond with in order, for repeated requests.
respx.get("https://example.org/").mock(side_effect=...)
You can also use the <route>.side_effect
setter.
route = respx.get("https://example.org/")
route.side_effect = ...
Functions
Function side effects will be called with the captured request
argument, and should either...
- return a mocked Response.
- raise an
Exception
to simulate a request error. - return
None
to treat the route as a non-match, continuing testing further routes. - return the input
Request
to pass through.
import httpx
import respx
def my_side_effect(request):
return httpx.Response(201)
@respx.mock
def test_side_effect():
respx.post("https://example.org/").mock(side_effect=my_side_effect)
response = httpx.post("https://example.org/")
assert response.status_code == 201
Optionally, a side effect can include a route
argument for cases where call stats,
or modifying the route within the side effect, is needed.
import httpx
import respx
def my_side_effect(request, route):
return httpx.Response(201, json={"id": route.call_count + 1})
@respx.mock
def test_side_effect():
respx.post("https://example.org/").mock(side_effect=my_side_effect)
response = httpx.post("https://example.org/")
assert response.json() == {"id": 1}
response = httpx.post("https://example.org/")
assert response.json() == {"id": 2}
If any of the route patterns are using a regex lookup, containing named groups, the regex groups will be passed as kwargs to the side effect.
import httpx
import respx
def my_side_effect(request, slug):
return httpx.Response(200, json={"slug": slug})
@respx.mock
def test_side_effect_kwargs():
route = respx.route(url__regex=r"https://example.org/(?P<slug>\w+)/")
route.side_effect = my_side_effect
response = httpx.get("https://example.org/foobar/")
assert response.status_code == 200
assert response.json() == {"slug": "foobar"}
A route can even decorate the function to be used as side effect.
import httpx
import rexpx
@respx.route(url__regex=r"https://example.org/(?P<user>\w+)/", name="user")
def user_api(request, user):
return httpx.Response(200, json={"user": user})
@respx.mock
def test_user_api():
response = httpx.get("https://example.org/lundberg/")
assert response.status_code == 200
assert response.json() == {"user": "lundberg"}
assert respx.routes["user"].called
Exceptions
To simulate a request error, pass a httpx.HTTPError subclass, or any Exception
as side effect.
import httpx
import respx
@respx.mock
def test_connection_error():
respx.get("https://example.org/").mock(side_effect=httpx.ConnectError)
with pytest.raises(httpx.ConnectError):
httpx.get("https://example.org/")
Iterable
If the side effect is an iterable, each repeated request will get the next Response returned, or exception raised, from the iterable.
import httpx
import respx
@respx.mock
def test_stacked_responses():
route = respx.get("https://example.org/")
route.side_effect = [
httpx.Response(404),
httpx.Response(200),
]
response1 = httpx.get("https://example.org/")
response2 = httpx.get("https://example.org/")
assert response1.status_code == 404
assert response2.status_code == 200
assert route.call_count == 2
Once the iterable is exhausted, the route will fallback and respond with the return_value
, if set.
import httpx
import respx
@respx.mock
def test_stacked_responses():
respx.post("https://example.org/").mock(
side_effect=[httpx.Response(201)],
return_value=httpx.Response(200)
)
response1 = httpx.post("https://example.org/")
response2 = httpx.post("https://example.org/")
response3 = httpx.post("https://example.org/")
assert response1.status_code == 201
assert response2.status_code == 200
assert response3.status_code == 200
Shortcuts
Respond
For convenience, <route>.respond(...)
can be used as a shortcut to return_value
.
respx.post("https://example.org/").respond(201)
See .respond() reference for more details.
Modulo
For simple mocking, a quick way is to use the python modulo (%
) operator to mock the response.
The right-hand modulo argument can either be ...
An int
representing the status_code
to mock:
respx.get("https://example.org/") % 204
response = httpx.get("https://example.org/")
assert response.status_code == 204
A dict
used as kwargs to create a mocked HTTPX
Response, with status code 200
by default:
respx.get("https://example.org/") % dict(json={"foo": "bar"})
response = httpx.get("https://example.org/")
assert response.status_code == 200
assert response.json() == {"foo": "bar"}
A HTTPX
Response object:
respx.get("https://example.org/") % Response(418)
response = httpx.get("https://example.org/")
assert response.status_code == httpx.codes.IM_A_TEAPOT
Rollback
When exiting a decorated test case, or context manager, the routes and their mocked values, i.e. return_value
and side_effect
, will be rolled back and restored to their initial state.
This means that you can safely modify existing routes, or add new ones, within a test case, without affecting other tests that are using the same router.
import httpx
import respx
# Initial routes
mock_router = respx.mock(base_url="https://example.org")
mock_router.get(path__regex="/user/(?P<pk>\d+)/", name="user") % 404
...
@mock_router
def test_user_exists():
# This change will be rolled back after this test case
mock_router["user"].return_value = httpx.Response(200)
response = httpx.get("https://example.org/user/123/")
assert response.status_code == 200
@mock_router
def test_user_not_found():
response = httpx.get("https://example.org/user/123/")
assert response.status_code == 404
Pass Through
If you want a route to not capture and mock a request response, use .pass_through()
.
import httpx
import respx
@respx.mock
def test_remote_response():
respx.route(host="localhost").pass_through()
response = httpx.get("http://localhost:8000/") # response from server
See .pass_through() reference for more details.
Mock without patching HTTPX
If you don't need to patch HTTPX
, use httpx.MockTransport
with a REPX router as handler, when instantiating your client.
import httpx
import respx
router = respx.Router()
router.post("https://example.org/") % 404
def test_client():
mock_transport = httpx.MockTransport(router.handler)
with httpx.Client(transport=mock_transport) as client:
response = client.post("https://example.org/")
assert response.status_code == 404
def test_client():
mock_transport = httpx.MockTransport(router.async_handler)
with httpx.AsyncClient(transport=mock_transport) as client:
...
NOTE
To assert all routes is called, you'll need to trigger
<router>.assert_all_called()
manually, e.g. in a test case or after yielding the
router in a pytest fixture, since there's no auto post assertion done like
when using respx.mock.
Hint
You can use RESPX
not only to mock out HTTPX
, but actually mock any library using HTTP Core
transports.
Call History
The respx
API includes a .calls
object, containing captured (request
, response
) named tuples and MagicMock's bells and whistles, i.e. call_count
, assert_called
etc.
Asserting calls
assert respx.calls.called
assert respx.calls.call_count == 1
respx.calls.assert_called()
respx.calls.assert_not_called()
respx.calls.assert_called_once()
Retrieving mocked calls
A matched and mocked Call
can be retrieved from call history, by either unpacking...
request, response = respx.calls.last
request, response = respx.calls[-2] # by call order
...or by accessing request
or response
directly...
last_request = respx.calls.last.request
assert json.loads(last_request.content) == {"foo": "bar"}
last_response = respx.calls.last.response
assert last_response.status_code == 200
Local route calls
Each Route
object has its own .calls
, along with .called
and .call_count
shortcuts.
import httpx
import respx
@respx.mock
def test_route_call_stats():
route = respx.post("https://example.org/baz/") % 201
httpx.post("https://example.org/baz/")
assert route.calls.last.request.url.path == "/baz/"
assert route.calls.last.response.status_code == 201
assert route.called
assert route.call_count == 1
route.calls.assert_called_once()
Reset History
The call history will automatically reset when exiting mocked context, i.e. leaving a decorated test case, or context manager scope.
To manually reset call stats during a test case, use respx.reset()
or <your_router>.reset()
.
import httpx
import respx
@respx.mock
def test_reset():
respx.post("https://foo.bar/baz/")
httpx.post("https://foo.bar/baz/")
assert respx.calls.call_count == 1
respx.calls.assert_called_once()
respx.reset()
assert len(respx.calls) == 0
assert respx.calls.call_count == 0
respx.calls.assert_not_called()