Respx Integration¶
This library can be used to load http responses from files into the Respx package, used for mocking responses with the Httpx package. This chapter is almost identical to the chapter on Responses integration. If you are already familiar with the Responses integration you can just read the Differences below.
Differences from Responses Integration
Specify the extra
pytest-scenario-files[respx]
for installation.Use the command line flags
--psf-load-respx
,--psf-assert-all-called
, and--psf-assert-all-mocked
to load and configure the mocking.Use the fixture
psf_respx_mock
.The keys used by
respx
to specify the response are a little bit different.
Responses key |
Respx key |
---|---|
status |
status_code |
body |
text |
content_type function arg |
content_type header |
Replacing a response in Respx is different from Responses. There are no specific methods like
replace()
orupsert()
. Instead, you overwrite an existing response by setting a new response route with the same HTTP method and URL.Responses will automatically queue up successive responses to the same method and URL and feed them to the calling test in order. Multiple responses for the same method and URL must be specified explicitly in Respx.
Basic Usage¶
There are three steps to using the Respx integration:
Create the data files.
Pass the
psf_respx_mock
fixture as a parameter to your test function.Activate the Respx integration using the command line flags.
Data file format¶
Data to be loaded into responses should be put into fixtures whose names
end with _response
or _responses
. For example, you might have a
fixture named oauth2_response
or a fixture named api_responses
.
These fixtures will not actually be created or parameterized for the
test. Instead, each will be loaded into the psf_respx_mock
fixture
and removed from the list of fixtures.
Each response should be structured as a dictionary that contains two required and four optional keys:
scenario_1:
oauth2_response:
method: GET|POST|PUT|etc. (required)
url: https://www.example.com/rest/endpoint (required)
status_code: 200 (optional)
text: Text body of the http response (optional)
json: (optional)
key1: value1
key2: value2
key3: value3
headers: (optional)
content_type: text/plain
header1: header-value-1
header2: header-value-2
text
andjson
are mutually exclusive and you should only use one of the two in a response.If you are passing in
json
you should not set acontent-type
header as it will be set toapplication/json
automatically.
The fixture may contain a list of responses in the same format:
scenario_2:
api_responses:
- method: GET|POST|PUT|etc.
url: https://www.example.com/rest/endpoint
text: Text body of the http response
- method: GET|POST|PUT|etc.
url: https://www.example.com/rest/endpoint2
text: Text body of the http response2
- method: GET|POST|PUT|etc.
url: https://www.example.com/rest/endpoint3
text: Text body of the http response3
All of the responses in the list will be loaded into a MockRouter. While
the response loading recognizes both _response
and _responses
,
there is no actual difference in how they are handled. The underlying
code checks to see whether the loaded value is a dict or a list and
handles it accordingly. Having both suffixes is just to make reading
the data files easier for humans.
The psf_respx_mock
fixture¶
Pytest-Scenario-Files provides a psf_respx_mock
fixture that is used
to load the responses. It returns a currently active respx.MockRouter
object
that has all of the responses from the data files for the current test
already loaded. If all of the responses your test needs are already loaded
via the data files you can just leave it be. However, If you need to add
additional responses or to change a response for this particular test you
can use it as you would any standard MockRouter
.
def test_api_call(psf_httpx_mock):
with httpx.Client() as client:
http_result = client.get("https://www.example.com/rest/endpoint")
assert http_result.status_code = 200
Command line flags¶
There are three command line flags for Pytest that are used for the Respx integration:
--psf-load-respx
This turns on the integration. Since the fixtures intended for use with Respx integration are marked by a special suffix, the integration should be explicitly triggered to avoid accidentally activating it for a developer who uses the suffix without realizing the special meaning.
--psf-assert-all-called=[true|FALSE]
This allows you to turn on the flag
assert_all_called
for Respx. It defaults to false.--psf-assert-all-mocked=[true|FALSE]
This allows you to turn on the flag
assert_all_mocked
for Respx. It defaults to false.
Advanced Usage¶
Overriding a response¶
You can use the psf_respx_mock
fixture to override a response for
a particular test. The replacement can be done in a separate fixture or
in the test function itself. If you are doing this in a separate
fixture the convention is to return the MockRouter
as the fixture
value so that you can chain together multiple fixtures that add or
alter the responses for a test.
@pytest.fixture
def alt_response_mock(psf_respx_mock):
psf_respx_mock.route(
method="GET",
url="https://www.example.com/rest/endpoint3"
).respond(status_code=200, text="Alternate response 3.")
return psf_respx_mock
def test_endpoint_3_error(alt_response_mock):
with httpx.Client() as client:
http_result = client.get("https://www.example.com/rest/endpoint3")
assert http_result.text == "Alternate response 3."
data_endpoint_3_error.yaml
¶api_call_scenario:
api_responses:
- method: GET
url: https://www.example.com/rest/endpoint
body: Text body of the http response
- method: GET
url: https://www.example.com/rest/endpoint2
body: Text body of the http response2
- method: GET
url: https://www.example.com/rest/endpoint3
body: Text body of the http response3
Multiple Responses for the Same URL¶
There are some test cases where you would want to call the same URL multiple times. For example, you may need to call a reset endpoint several times as part of a sequence of tasks; or you may be polling an endpoint to see if a process has been completed.
If you put a single response in for a method and URL, Respx will reply to repeated requests to that URL with the same response.
If you want to have different responses to the same method and URL you can put the desired responses into a list of responses in the data file, all with the same method and URL. Pytest-Scenario-Files will load them into the proper place in the MockRouter to respond accordingly. The order of responses is guaranteed if they are within the same list of responses, but the order is not guaranteed between lists of responses.
Using the following data file will return a status code of 202 and a json block
with process_completed = false
three times, followed by a status code of
200 and a json block with process_completed = true
. If the test does a GET
on the URL for a fifth time it will cause a StopIteration exception, as the
list of responses would be exhausted.
data_api_polling_test.yaml
¶api_polling_scenario:
api_responses:
- method: GET
url: https://www.example.com/rest/process_done
status_code: 202
json:
process_completed: false
- method: GET
url: https://www.example.com/rest/process_done
status_code: 202
json:
process_completed: false
- method: GET
url: https://www.example.com/rest/process_done
status_code: 202
json:
process_completed: false
- method: GET
url: https://www.example.com/rest/process_done
status_code: 200
json:
process_completed: true
Note
Pytest-Scenario-Files does not include a way to specify that the last
response should be repeated forever. The Respx documentation suggests
that this can be accomplished by using the library functions
itertools.chain()
and itertools.repeat()
together. When using
Pytest-Scenario-Files the recommended way to handle this is to create your
own response override fixture that will set up the proper iteration.
Usage with the psf_expected_result
fixture¶
You can set up a data file with the generally expected response for a specific URL, then override the response to check error conditions. Here is an example using a file with the standard API response and a test that checks both a successful and an unsuccessful test of the API.
This first file contains the basic API responses, which are loaded by reference for each scenario:
all_api_responses.yaml
¶api_testing:
api_responses:
- url: https://www.example.com/rest/endpoint
method: GET
status_code: 200
body: The call was successful.
The second file contains the scenarios, success and failure. The success
scenario just runs through the call and contains no overrides. The failure
scenario specifies that the call should return a 403 error and catch a
httpx.HTTPError
exception:
data_api_check_full.yaml
¶success_scenario:
api_responses: __all_api_responses.yaml:api_testing:api_responses
psf_expected_result_indirect: The call was successful.
failure_scenario:
api_responses: __all_api_responses.yaml:api_testing:api_responses
response_override_indirect:
url: https://www.example.com/rest/endpoint
method: GET
status_code: 403
text: Access denied.
psf_expected_result_indirect:
expected_exception_type: httpx.HTTPError
The third file is the Python unit tests. It has a fixture response_override()
that will set up an override specified by the scenario. If the scenario
has no override then it will just return the psf_respx_mock
fixture
unchanged.
test_api.py
¶@pytest.fixture
def response_override(request, psf_respx_mock):
if hasattr(request, "param") and isinstance(request.param, dict):
response_params = request.param.copy()
route_match = {k: response_params.pop(k) for k in ("method", "url")}
respx_mock.route(**route_match).respond(**response_params)
return psf_respx_mock
def test_api_check(response_override, psf_expected_result):
with psf_expected_result as expected_result:
with httpx.Client() as client:
http_result = client.get("https://www.example.com/rest/endpoint3")
api_call_result.raise_for_status()
assert api_call_result.body == "The call was successful."
When the test is run the first time (success_scenario
), Respx will
return a 200 response with a body of “The call was successful.” — which is
the expected value from the psf_expected_result
fixture.
When the test is run the second time (failure_scenario
), Respx will
return a 403 response. raise_for_status()
will then raise an exception
httpx.HTTPError
, which will be caught by the context manager since
the psf_expected_value
fixture will return a pytest.raises(httpx.HTTPError)
context manager object. Any other kind of error or exception will cause the
test to fail.