diff options
author | CoprDistGit <infra@openeuler.org> | 2023-03-09 16:04:49 +0000 |
---|---|---|
committer | CoprDistGit <infra@openeuler.org> | 2023-03-09 16:04:49 +0000 |
commit | f3b399d2b3c75e8ea516b0d7cead909df8c6fd4c (patch) | |
tree | 9594587abaa6aaa23452da414c06b4b93853ac6b /python-pytest-httpx.spec | |
parent | d9a5a2b3df375ff1516b749333baf15d4dc461bf (diff) |
automatic import of python-pytest-httpx
Diffstat (limited to 'python-pytest-httpx.spec')
-rw-r--r-- | python-pytest-httpx.spec | 2254 |
1 files changed, 2254 insertions, 0 deletions
diff --git a/python-pytest-httpx.spec b/python-pytest-httpx.spec new file mode 100644 index 0000000..aa25a43 --- /dev/null +++ b/python-pytest-httpx.spec @@ -0,0 +1,2254 @@ +%global _empty_manifest_terminate_build 0 +Name: python-pytest-httpx +Version: 0.21.3 +Release: 1 +Summary: Send responses to httpx. +License: MIT +URL: https://colin-b.github.io/pytest_httpx/ +Source0: https://mirrors.nju.edu.cn/pypi/web/packages/13/53/0a8711ae619fcfd5ca1b526616ec8e65ef33e33d5eb4668a938db3d0b919/pytest_httpx-0.21.3.tar.gz +BuildArch: noarch + +Requires: python3-httpx +Requires: python3-pytest +Requires: python3-pytest-asyncio +Requires: python3-pytest-cov + +%description +<h2 align="center">Send responses to HTTPX using pytest</h2> + +<p align="center"> +<a href="https://pypi.org/project/pytest-httpx/"><img alt="pypi version" src="https://img.shields.io/pypi/v/pytest_httpx"></a> +<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Build status" src="https://github.com/Colin-b/pytest_httpx/workflows/Release/badge.svg"></a> +<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a> +<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a> +<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-168 passed-blue"></a> +<a href="https://pypi.org/project/pytest-httpx/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/pytest_httpx"></a> +</p> + +> Version 1.0.0 will be released once httpx is considered as stable (release of 1.0.0). +> +> However, current state can be considered as stable. + +Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixture will make sure every [`httpx`](https://www.python-httpx.org) request will be replied to with user provided responses. + +- [Add responses](#add-responses) + - [JSON body](#add-json-response) + - [Custom body](#reply-with-custom-body) + - [Multipart body (files, ...)](#add-multipart-response) + - [HTTP status code](#add-non-200-response) + - [HTTP headers](#reply-with-custom-headers) + - [HTTP/2.0](#add-http/2.0-response) +- [Add dynamic responses](#dynamic-responses) +- [Raising exceptions](#raising-exceptions) +- [Check requests](#check-sent-requests) +- [Do not mock some requests](#do-not-mock-some-requests) +- [Migrating](#migrating-to-pytest-httpx) + - [responses](#from-responses) + - [aioresponses](#from-aioresponses) + +## Add responses + +You can register responses for both sync and async [`HTTPX`](https://www.python-httpx.org) requests. + +```python +import pytest +import httpx + + +def test_something(httpx_mock): + httpx_mock.add_response() + + with httpx.Client() as client: + response = client.get("https://test_url") + + +@pytest.mark.asyncio +async def test_something_async(httpx_mock): + httpx_mock.add_response() + + async with httpx.AsyncClient() as client: + response = await client.get("https://test_url") +``` + +If all registered responses are not sent back during test execution, the test case will fail at teardown. + +This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture: + +```python +import pytest + +@pytest.fixture +def assert_all_responses_were_requested() -> bool: + return False +``` + +Default response is a HTTP/1.1 200 (OK) without any body. + +### How response is selected + +In case more than one response match request, the first one not yet sent (according to the registration order) will be sent. + +In case all matching responses have been sent, the last one (according to the registration order) will be sent. + +You can add criteria so that response will be sent only in case of a more specific matching. + +#### Matching on URL + +`url` parameter can either be a string, a python [re.Pattern](https://docs.python.org/3/library/re.html) instance or a [httpx.URL](https://www.python-httpx.org/api/#url) instance. + +Matching is performed on the full URL, query parameters included. + +Order of parameters in the query string does not matter, however order of values do matter if the same parameter is provided more than once. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_url(httpx_mock: HTTPXMock): + httpx_mock.add_response(url="https://test_url?a=1&b=2") + + with httpx.Client() as client: + response1 = client.delete("https://test_url?a=1&b=2") + response2 = client.get("https://test_url?b=2&a=1") +``` + +#### Matching on HTTP method + +Use `method` parameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) to reply to. + +`method` parameter must be a string. It will be upper-cased, so it can be provided lower cased. + +Matching is performed on equality. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_post(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="POST") + + with httpx.Client() as client: + response = client.post("https://test_url") + + +def test_put(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="PUT") + + with httpx.Client() as client: + response = client.put("https://test_url") + + +def test_delete(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="DELETE") + + with httpx.Client() as client: + response = client.delete("https://test_url") + + +def test_patch(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="PATCH") + + with httpx.Client() as client: + response = client.patch("https://test_url") + + +def test_head(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="HEAD") + + with httpx.Client() as client: + response = client.head("https://test_url") + +``` + +#### Matching on HTTP headers + +Use `match_headers` parameter to specify the HTTP headers to reply to. + +Matching is performed on equality for each provided header. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_headers_matching(httpx_mock: HTTPXMock): + httpx_mock.add_response(match_headers={'user-agent': 'python-httpx/0.23.0'}) + + with httpx.Client() as client: + response = client.get("https://test_url") +``` + +#### Matching on HTTP body + +Use `match_content` parameter to specify the full HTTP body to reply to. + +Matching is performed on equality. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_content_matching(httpx_mock: HTTPXMock): + httpx_mock.add_response(match_content=b"This is the body") + + with httpx.Client() as client: + response = client.post("https://test_url", content=b"This is the body") +``` + +### Add JSON response + +Use `json` parameter to add a JSON response using python values. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_json(httpx_mock: HTTPXMock): + httpx_mock.add_response(json=[{"key1": "value1", "key2": "value2"}]) + + with httpx.Client() as client: + assert client.get("https://test_url").json() == [{"key1": "value1", "key2": "value2"}] + +``` + +Note that the `content-type` header will be set to `application/json` by default in the response. + +### Reply with custom body + +Use `text` parameter to reply with a custom body by providing UTF-8 encoded string. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_str_body(httpx_mock: HTTPXMock): + httpx_mock.add_response(text="This is my UTF-8 content") + + with httpx.Client() as client: + assert client.get("https://test_url").text == "This is my UTF-8 content" + +``` + +Use `content` parameter to reply with a custom body by providing bytes. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_bytes_body(httpx_mock: HTTPXMock): + httpx_mock.add_response(content=b"This is my bytes content") + + with httpx.Client() as client: + assert client.get("https://test_url").content == b"This is my bytes content" + +``` + +Use `html` parameter to reply with a custom body by providing UTF-8 encoded string. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_html_body(httpx_mock: HTTPXMock): + httpx_mock.add_response(html="<body>This is <p> HTML content</body>") + + with httpx.Client() as client: + assert client.get("https://test_url").text == "<body>This is <p> HTML content</body>" + +``` + +### Reply by streaming chunks + +Use `stream` parameter to stream chunks that you specify. + +```python +import httpx +import pytest +from pytest_httpx import HTTPXMock, IteratorStream + +def test_sync_streaming(httpx_mock: HTTPXMock): + httpx_mock.add_response(stream=IteratorStream([b"part 1", b"part 2"])) + + with httpx.Client() as client: + with client.stream(method="GET", url="https://test_url") as response: + assert list(response.iter_raw()) == [b"part 1", b"part 2"] + + +@pytest.mark.asyncio +async def test_async_streaming(httpx_mock: HTTPXMock): + httpx_mock.add_response(stream=IteratorStream([b"part 1", b"part 2"])) + + async with httpx.AsyncClient() as client: + async with client.stream(method="GET", url="https://test_url") as response: + assert [part async for part in response.aiter_raw()] == [b"part 1", b"part 2"] + +``` + +### Add multipart response + +Use the httpx `MultipartStream` via the `stream` parameter to send a multipart response. + +Reach out to `httpx` developers if you need this publicly exposed as [this is not a standard use case](https://github.com/encode/httpx/issues/872#issuecomment-633584819). + +```python +import httpx +from httpx._multipart import MultipartStream +from pytest_httpx import HTTPXMock + + +def test_multipart_body(httpx_mock: HTTPXMock): + httpx_mock.add_response(stream=MultipartStream(data={"key1": "value1"}, files={"file1": b"content of file 1"}, boundary=b"2256d3a36d2a61a1eba35a22bee5c74a")) + + with httpx.Client() as client: + assert client.get("https://test_url").text == '''--2256d3a36d2a61a1eba35a22bee5c74a\r +Content-Disposition: form-data; name="key1"\r +\r +value1\r +--2256d3a36d2a61a1eba35a22bee5c74a\r +Content-Disposition: form-data; name="file1"; filename="upload"\r +Content-Type: application/octet-stream\r +\r +content of file 1\r +--2256d3a36d2a61a1eba35a22bee5c74a--\r +''' + +``` + +### Add non 200 response + +Use `status_code` parameter to specify the HTTP status code of the response. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_status_code(httpx_mock: HTTPXMock): + httpx_mock.add_response(status_code=404) + + with httpx.Client() as client: + assert client.get("https://test_url").status_code == 404 + +``` + +### Reply with custom headers + +Use `headers` parameter to specify the extra headers of the response. + +Any valid httpx headers type is supported, you can submit headers as a dict (str or bytes), a list of 2-tuples (str or bytes) or a `httpx.Header` instance. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_headers_as_str_dict(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers={"X-Header1": "Test value"}) + + with httpx.Client() as client: + assert client.get("https://test_url").headers["x-header1"] == "Test value" + + +def test_headers_as_str_tuple_list(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers=[("X-Header1", "Test value")]) + + with httpx.Client() as client: + assert client.get("https://test_url").headers["x-header1"] == "Test value" + + +def test_headers_as_httpx_headers(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers=httpx.Headers({b"X-Header1": b"Test value"})) + + with httpx.Client() as client: + assert client.get("https://test_url").headers["x-header1"] == "Test value" + +``` + +#### Reply with cookies + +Cookies are sent in the `set-cookie` HTTP header. + +You can then send cookies in the response by setting the `set-cookie` header with [the value following key=value format]((https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)). + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_cookie(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers={"set-cookie": "key=value"}) + + with httpx.Client() as client: + response = client.get("https://test_url") + assert dict(response.cookies) == {"key": "value"} + + +def test_cookies(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers=[("set-cookie", "key=value"), ("set-cookie", "key2=value2")]) + + with httpx.Client() as client: + response = client.get("https://test_url") + assert dict(response.cookies) == {"key": "value", "key2": "value2"} + +``` + + +### Add HTTP/2.0 response + +Use `http_version` parameter to specify the HTTP protocol version of the response. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_http_version(httpx_mock: HTTPXMock): + httpx_mock.add_response(http_version="HTTP/2.0") + + with httpx.Client() as client: + assert client.get("https://test_url").http_version == "HTTP/2.0" + +``` + +## Add callbacks + +You can perform custom manipulation upon request reception by registering callbacks. + +Callback should expect one parameter, the received [`httpx.Request`](https://www.python-httpx.org/api/#request). + +If all callbacks are not executed during test execution, the test case will fail at teardown. + +This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture: + +```python +import pytest + +@pytest.fixture +def assert_all_responses_were_requested() -> bool: + return False +``` + +Note that callbacks are considered as responses, and thus are [selected the same way](#how-response-is-selected). + +### Dynamic responses + +Callback should return a `httpx.Response`. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_dynamic_response(httpx_mock: HTTPXMock): + def custom_response(request: httpx.Request): + return httpx.Response( + status_code=200, json={"url": str(request.url)}, + ) + + httpx_mock.add_callback(custom_response) + + with httpx.Client() as client: + response = client.get("https://test_url") + assert response.json() == {"url": "https://test_url"} + +``` + +Alternatively, callbacks can also be asynchronous. + +As in the following sample simulating network latency on some responses only. + +```python +import asyncio +import httpx +import pytest +from pytest_httpx import HTTPXMock + + +@pytest.mark.asyncio +async def test_dynamic_async_response(httpx_mock: HTTPXMock): + async def simulate_network_latency(request: httpx.Request): + await asyncio.sleep(1) + return httpx.Response( + status_code=200, json={"url": str(request.url)}, + ) + + httpx_mock.add_callback(simulate_network_latency) + httpx_mock.add_response() + + async with httpx.AsyncClient() as client: + responses = await asyncio.gather( + # Response will be received after one second + client.get("https://test_url"), + # Response will instantly be received (1 second before the first request) + client.get("https://test_url") + ) + +``` + +### Raising exceptions + +You can simulate HTTPX exception throwing by raising an exception in your callback or use `httpx_mock.add_exception` with the exception instance. + +This can be useful if you want to assert that your code handles HTTPX exceptions properly. + +```python +import httpx +import pytest +from pytest_httpx import HTTPXMock + + +def test_exception_raising(httpx_mock: HTTPXMock): + httpx_mock.add_exception(httpx.ReadTimeout("Unable to read within timeout")) + + with httpx.Client() as client: + with pytest.raises(httpx.ReadTimeout): + client.get("https://test_url") + +``` + +Note that default behavior is to send an `httpx.TimeoutException` in case no response can be found. You can then test this kind of exception this way: + +```python +import httpx +import pytest +from pytest_httpx import HTTPXMock + + +def test_timeout(httpx_mock: HTTPXMock): + with httpx.Client() as client: + with pytest.raises(httpx.TimeoutException): + client.get("https://test_url") + +``` + +## Check sent requests + +The best way to ensure the content of your requests is still to use the `match_headers` and / or `match_content` parameters when adding a response. +In the same spirit, ensuring that no request was issued does not necessarily requires any code. + +In any case, you always have the ability to retrieve the requests that were issued. + +As in the following samples: + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_many_requests(httpx_mock: HTTPXMock): + httpx_mock.add_response() + + with httpx.Client() as client: + response1 = client.get("https://test_url") + response2 = client.get("https://test_url") + + requests = httpx_mock.get_requests() + + +def test_single_request(httpx_mock: HTTPXMock): + httpx_mock.add_response() + + with httpx.Client() as client: + response = client.get("https://test_url") + + request = httpx_mock.get_request() + + +def test_no_request(httpx_mock: HTTPXMock): + assert not httpx_mock.get_request() +``` + +### How requests are selected + +You can add criteria so that requests will be returned only in case of a more specific matching. + +#### Matching on URL + +`url` parameter can either be a string, a python [re.Pattern](https://docs.python.org/3/library/re.html) instance or a [httpx.URL](https://www.python-httpx.org/api/#url) instance. + +Matching is performed on the full URL, query parameters included. + +#### Matching on HTTP method + +Use `method` parameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) of the requests to retrieve. + +`method` parameter must be a string. It will be upper-cased, so it can be provided lower cased. + +Matching is performed on equality. + +#### Matching on HTTP headers + +Use `match_headers` parameter to specify the HTTP headers executing the callback. + +Matching is performed on equality for each provided header. + +#### Matching on HTTP body + +Use `match_content` parameter to specify the full HTTP body executing the callback. + +Matching is performed on equality. + +## Do not mock some requests + +By default, `pytest-httpx` will mock every request. + +But, for instance, in case you want to write integration tests with other servers, you might want to let some requests go through. + +To do so, you can use the `non_mocked_hosts` fixture: + +```python +import pytest + +@pytest.fixture +def non_mocked_hosts() -> list: + return ["my_local_test_host", "my_other_test_host"] +``` + +Every other requested hosts will be mocked as in the following example + +```python +import pytest +import httpx + +@pytest.fixture +def non_mocked_hosts() -> list: + return ["my_local_test_host"] + + +def test_partial_mock(httpx_mock): + httpx_mock.add_response() + + with httpx.Client() as client: + # This request will NOT be mocked + response1 = client.get("https://www.my_local_test_host/sub?param=value") + # This request will be mocked + response2 = client.get("https://test_url") +``` + +## Migrating to pytest-httpx + +Here is how to migrate from well-known testing libraries to `pytest-httpx`. + +### From responses + +| Feature | responses | pytest-httpx | +|:------------------|:---------------------------|:----------------------------| +| Add a response | `responses.add()` | `httpx_mock.add_response()` | +| Add a callback | `responses.add_callback()` | `httpx_mock.add_callback()` | +| Retrieve requests | `responses.calls` | `httpx_mock.get_requests()` | + +#### Add a response or a callback + +Undocumented parameters means that they are unchanged between `responses` and `pytest-httpx`. +Below is a list of parameters that will require a change in your code. + +| Parameter | responses | pytest-httpx | +|:---------------------|:------------------------------------|:---------------------------------------------------------------------| +| method | `method=responses.GET` | `method="GET"` | +| body (as bytes) | `body=b"sample"` | `content=b"sample"` | +| body (as str) | `body="sample"` | `text="sample"` | +| status code | `status=201` | `status_code=201` | +| headers | `adding_headers={"name": "value"}` | `headers={"name": "value"}` | +| content-type header | `content_type="application/custom"` | `headers={"content-type": "application/custom"}` | +| Match the full query | `match_querystring=True` | The full query is always matched when providing the `url` parameter. | + +Sample adding a response with `responses`: +```python +from responses import RequestsMock + +def test_response(responses: RequestsMock): + responses.add( + method=responses.GET, + url="https://test_url", + body=b"This is the response content", + status=400, + ) + +``` + +Sample adding the same response with `pytest-httpx`: +```python +from pytest_httpx import HTTPXMock + +def test_response(httpx_mock: HTTPXMock): + httpx_mock.add_response( + method="GET", + url="https://test_url", + content=b"This is the response content", + status_code=400, + ) + +``` + +### From aioresponses + +| Feature | aioresponses | pytest-httpx | +|:---------------|:------------------------|:-------------------------------------------| +| Add a response | `aioresponses.method()` | `httpx_mock.add_response(method="METHOD")` | +| Add a callback | `aioresponses.method()` | `httpx_mock.add_callback(method="METHOD")` | + +#### Add a response or a callback + +Undocumented parameters means that they are unchanged between `responses` and `pytest-httpx`. +Below is a list of parameters that will require a change in your code. + +| Parameter | responses | pytest-httpx | +|:----------------|:---------------------|:--------------------| +| body (as bytes) | `body=b"sample"` | `content=b"sample"` | +| body (as str) | `body="sample"` | `text="sample"` | +| body (as JSON) | `payload=["sample"]` | `json=["sample"]` | +| status code | `status=201` | `status_code=201` | + +Sample adding a response with `aioresponses`: +```python +import pytest +from aioresponses import aioresponses + + +@pytest.fixture +def mock_aioresponse(): + with aioresponses() as m: + yield m + + +def test_response(mock_aioresponse): + mock_aioresponse.get( + url="https://test_url", + body=b"This is the response content", + status=400, + ) + +``` + +Sample adding the same response with `pytest-httpx`: +```python +def test_response(httpx_mock): + httpx_mock.add_response( + method="GET", + url="https://test_url", + content=b"This is the response content", + status_code=400, + ) + +``` + + + + +%package -n python3-pytest-httpx +Summary: Send responses to httpx. +Provides: python-pytest-httpx +BuildRequires: python3-devel +BuildRequires: python3-setuptools +BuildRequires: python3-pip +%description -n python3-pytest-httpx +<h2 align="center">Send responses to HTTPX using pytest</h2> + +<p align="center"> +<a href="https://pypi.org/project/pytest-httpx/"><img alt="pypi version" src="https://img.shields.io/pypi/v/pytest_httpx"></a> +<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Build status" src="https://github.com/Colin-b/pytest_httpx/workflows/Release/badge.svg"></a> +<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a> +<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a> +<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-168 passed-blue"></a> +<a href="https://pypi.org/project/pytest-httpx/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/pytest_httpx"></a> +</p> + +> Version 1.0.0 will be released once httpx is considered as stable (release of 1.0.0). +> +> However, current state can be considered as stable. + +Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixture will make sure every [`httpx`](https://www.python-httpx.org) request will be replied to with user provided responses. + +- [Add responses](#add-responses) + - [JSON body](#add-json-response) + - [Custom body](#reply-with-custom-body) + - [Multipart body (files, ...)](#add-multipart-response) + - [HTTP status code](#add-non-200-response) + - [HTTP headers](#reply-with-custom-headers) + - [HTTP/2.0](#add-http/2.0-response) +- [Add dynamic responses](#dynamic-responses) +- [Raising exceptions](#raising-exceptions) +- [Check requests](#check-sent-requests) +- [Do not mock some requests](#do-not-mock-some-requests) +- [Migrating](#migrating-to-pytest-httpx) + - [responses](#from-responses) + - [aioresponses](#from-aioresponses) + +## Add responses + +You can register responses for both sync and async [`HTTPX`](https://www.python-httpx.org) requests. + +```python +import pytest +import httpx + + +def test_something(httpx_mock): + httpx_mock.add_response() + + with httpx.Client() as client: + response = client.get("https://test_url") + + +@pytest.mark.asyncio +async def test_something_async(httpx_mock): + httpx_mock.add_response() + + async with httpx.AsyncClient() as client: + response = await client.get("https://test_url") +``` + +If all registered responses are not sent back during test execution, the test case will fail at teardown. + +This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture: + +```python +import pytest + +@pytest.fixture +def assert_all_responses_were_requested() -> bool: + return False +``` + +Default response is a HTTP/1.1 200 (OK) without any body. + +### How response is selected + +In case more than one response match request, the first one not yet sent (according to the registration order) will be sent. + +In case all matching responses have been sent, the last one (according to the registration order) will be sent. + +You can add criteria so that response will be sent only in case of a more specific matching. + +#### Matching on URL + +`url` parameter can either be a string, a python [re.Pattern](https://docs.python.org/3/library/re.html) instance or a [httpx.URL](https://www.python-httpx.org/api/#url) instance. + +Matching is performed on the full URL, query parameters included. + +Order of parameters in the query string does not matter, however order of values do matter if the same parameter is provided more than once. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_url(httpx_mock: HTTPXMock): + httpx_mock.add_response(url="https://test_url?a=1&b=2") + + with httpx.Client() as client: + response1 = client.delete("https://test_url?a=1&b=2") + response2 = client.get("https://test_url?b=2&a=1") +``` + +#### Matching on HTTP method + +Use `method` parameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) to reply to. + +`method` parameter must be a string. It will be upper-cased, so it can be provided lower cased. + +Matching is performed on equality. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_post(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="POST") + + with httpx.Client() as client: + response = client.post("https://test_url") + + +def test_put(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="PUT") + + with httpx.Client() as client: + response = client.put("https://test_url") + + +def test_delete(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="DELETE") + + with httpx.Client() as client: + response = client.delete("https://test_url") + + +def test_patch(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="PATCH") + + with httpx.Client() as client: + response = client.patch("https://test_url") + + +def test_head(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="HEAD") + + with httpx.Client() as client: + response = client.head("https://test_url") + +``` + +#### Matching on HTTP headers + +Use `match_headers` parameter to specify the HTTP headers to reply to. + +Matching is performed on equality for each provided header. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_headers_matching(httpx_mock: HTTPXMock): + httpx_mock.add_response(match_headers={'user-agent': 'python-httpx/0.23.0'}) + + with httpx.Client() as client: + response = client.get("https://test_url") +``` + +#### Matching on HTTP body + +Use `match_content` parameter to specify the full HTTP body to reply to. + +Matching is performed on equality. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_content_matching(httpx_mock: HTTPXMock): + httpx_mock.add_response(match_content=b"This is the body") + + with httpx.Client() as client: + response = client.post("https://test_url", content=b"This is the body") +``` + +### Add JSON response + +Use `json` parameter to add a JSON response using python values. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_json(httpx_mock: HTTPXMock): + httpx_mock.add_response(json=[{"key1": "value1", "key2": "value2"}]) + + with httpx.Client() as client: + assert client.get("https://test_url").json() == [{"key1": "value1", "key2": "value2"}] + +``` + +Note that the `content-type` header will be set to `application/json` by default in the response. + +### Reply with custom body + +Use `text` parameter to reply with a custom body by providing UTF-8 encoded string. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_str_body(httpx_mock: HTTPXMock): + httpx_mock.add_response(text="This is my UTF-8 content") + + with httpx.Client() as client: + assert client.get("https://test_url").text == "This is my UTF-8 content" + +``` + +Use `content` parameter to reply with a custom body by providing bytes. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_bytes_body(httpx_mock: HTTPXMock): + httpx_mock.add_response(content=b"This is my bytes content") + + with httpx.Client() as client: + assert client.get("https://test_url").content == b"This is my bytes content" + +``` + +Use `html` parameter to reply with a custom body by providing UTF-8 encoded string. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_html_body(httpx_mock: HTTPXMock): + httpx_mock.add_response(html="<body>This is <p> HTML content</body>") + + with httpx.Client() as client: + assert client.get("https://test_url").text == "<body>This is <p> HTML content</body>" + +``` + +### Reply by streaming chunks + +Use `stream` parameter to stream chunks that you specify. + +```python +import httpx +import pytest +from pytest_httpx import HTTPXMock, IteratorStream + +def test_sync_streaming(httpx_mock: HTTPXMock): + httpx_mock.add_response(stream=IteratorStream([b"part 1", b"part 2"])) + + with httpx.Client() as client: + with client.stream(method="GET", url="https://test_url") as response: + assert list(response.iter_raw()) == [b"part 1", b"part 2"] + + +@pytest.mark.asyncio +async def test_async_streaming(httpx_mock: HTTPXMock): + httpx_mock.add_response(stream=IteratorStream([b"part 1", b"part 2"])) + + async with httpx.AsyncClient() as client: + async with client.stream(method="GET", url="https://test_url") as response: + assert [part async for part in response.aiter_raw()] == [b"part 1", b"part 2"] + +``` + +### Add multipart response + +Use the httpx `MultipartStream` via the `stream` parameter to send a multipart response. + +Reach out to `httpx` developers if you need this publicly exposed as [this is not a standard use case](https://github.com/encode/httpx/issues/872#issuecomment-633584819). + +```python +import httpx +from httpx._multipart import MultipartStream +from pytest_httpx import HTTPXMock + + +def test_multipart_body(httpx_mock: HTTPXMock): + httpx_mock.add_response(stream=MultipartStream(data={"key1": "value1"}, files={"file1": b"content of file 1"}, boundary=b"2256d3a36d2a61a1eba35a22bee5c74a")) + + with httpx.Client() as client: + assert client.get("https://test_url").text == '''--2256d3a36d2a61a1eba35a22bee5c74a\r +Content-Disposition: form-data; name="key1"\r +\r +value1\r +--2256d3a36d2a61a1eba35a22bee5c74a\r +Content-Disposition: form-data; name="file1"; filename="upload"\r +Content-Type: application/octet-stream\r +\r +content of file 1\r +--2256d3a36d2a61a1eba35a22bee5c74a--\r +''' + +``` + +### Add non 200 response + +Use `status_code` parameter to specify the HTTP status code of the response. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_status_code(httpx_mock: HTTPXMock): + httpx_mock.add_response(status_code=404) + + with httpx.Client() as client: + assert client.get("https://test_url").status_code == 404 + +``` + +### Reply with custom headers + +Use `headers` parameter to specify the extra headers of the response. + +Any valid httpx headers type is supported, you can submit headers as a dict (str or bytes), a list of 2-tuples (str or bytes) or a `httpx.Header` instance. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_headers_as_str_dict(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers={"X-Header1": "Test value"}) + + with httpx.Client() as client: + assert client.get("https://test_url").headers["x-header1"] == "Test value" + + +def test_headers_as_str_tuple_list(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers=[("X-Header1", "Test value")]) + + with httpx.Client() as client: + assert client.get("https://test_url").headers["x-header1"] == "Test value" + + +def test_headers_as_httpx_headers(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers=httpx.Headers({b"X-Header1": b"Test value"})) + + with httpx.Client() as client: + assert client.get("https://test_url").headers["x-header1"] == "Test value" + +``` + +#### Reply with cookies + +Cookies are sent in the `set-cookie` HTTP header. + +You can then send cookies in the response by setting the `set-cookie` header with [the value following key=value format]((https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)). + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_cookie(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers={"set-cookie": "key=value"}) + + with httpx.Client() as client: + response = client.get("https://test_url") + assert dict(response.cookies) == {"key": "value"} + + +def test_cookies(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers=[("set-cookie", "key=value"), ("set-cookie", "key2=value2")]) + + with httpx.Client() as client: + response = client.get("https://test_url") + assert dict(response.cookies) == {"key": "value", "key2": "value2"} + +``` + + +### Add HTTP/2.0 response + +Use `http_version` parameter to specify the HTTP protocol version of the response. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_http_version(httpx_mock: HTTPXMock): + httpx_mock.add_response(http_version="HTTP/2.0") + + with httpx.Client() as client: + assert client.get("https://test_url").http_version == "HTTP/2.0" + +``` + +## Add callbacks + +You can perform custom manipulation upon request reception by registering callbacks. + +Callback should expect one parameter, the received [`httpx.Request`](https://www.python-httpx.org/api/#request). + +If all callbacks are not executed during test execution, the test case will fail at teardown. + +This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture: + +```python +import pytest + +@pytest.fixture +def assert_all_responses_were_requested() -> bool: + return False +``` + +Note that callbacks are considered as responses, and thus are [selected the same way](#how-response-is-selected). + +### Dynamic responses + +Callback should return a `httpx.Response`. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_dynamic_response(httpx_mock: HTTPXMock): + def custom_response(request: httpx.Request): + return httpx.Response( + status_code=200, json={"url": str(request.url)}, + ) + + httpx_mock.add_callback(custom_response) + + with httpx.Client() as client: + response = client.get("https://test_url") + assert response.json() == {"url": "https://test_url"} + +``` + +Alternatively, callbacks can also be asynchronous. + +As in the following sample simulating network latency on some responses only. + +```python +import asyncio +import httpx +import pytest +from pytest_httpx import HTTPXMock + + +@pytest.mark.asyncio +async def test_dynamic_async_response(httpx_mock: HTTPXMock): + async def simulate_network_latency(request: httpx.Request): + await asyncio.sleep(1) + return httpx.Response( + status_code=200, json={"url": str(request.url)}, + ) + + httpx_mock.add_callback(simulate_network_latency) + httpx_mock.add_response() + + async with httpx.AsyncClient() as client: + responses = await asyncio.gather( + # Response will be received after one second + client.get("https://test_url"), + # Response will instantly be received (1 second before the first request) + client.get("https://test_url") + ) + +``` + +### Raising exceptions + +You can simulate HTTPX exception throwing by raising an exception in your callback or use `httpx_mock.add_exception` with the exception instance. + +This can be useful if you want to assert that your code handles HTTPX exceptions properly. + +```python +import httpx +import pytest +from pytest_httpx import HTTPXMock + + +def test_exception_raising(httpx_mock: HTTPXMock): + httpx_mock.add_exception(httpx.ReadTimeout("Unable to read within timeout")) + + with httpx.Client() as client: + with pytest.raises(httpx.ReadTimeout): + client.get("https://test_url") + +``` + +Note that default behavior is to send an `httpx.TimeoutException` in case no response can be found. You can then test this kind of exception this way: + +```python +import httpx +import pytest +from pytest_httpx import HTTPXMock + + +def test_timeout(httpx_mock: HTTPXMock): + with httpx.Client() as client: + with pytest.raises(httpx.TimeoutException): + client.get("https://test_url") + +``` + +## Check sent requests + +The best way to ensure the content of your requests is still to use the `match_headers` and / or `match_content` parameters when adding a response. +In the same spirit, ensuring that no request was issued does not necessarily requires any code. + +In any case, you always have the ability to retrieve the requests that were issued. + +As in the following samples: + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_many_requests(httpx_mock: HTTPXMock): + httpx_mock.add_response() + + with httpx.Client() as client: + response1 = client.get("https://test_url") + response2 = client.get("https://test_url") + + requests = httpx_mock.get_requests() + + +def test_single_request(httpx_mock: HTTPXMock): + httpx_mock.add_response() + + with httpx.Client() as client: + response = client.get("https://test_url") + + request = httpx_mock.get_request() + + +def test_no_request(httpx_mock: HTTPXMock): + assert not httpx_mock.get_request() +``` + +### How requests are selected + +You can add criteria so that requests will be returned only in case of a more specific matching. + +#### Matching on URL + +`url` parameter can either be a string, a python [re.Pattern](https://docs.python.org/3/library/re.html) instance or a [httpx.URL](https://www.python-httpx.org/api/#url) instance. + +Matching is performed on the full URL, query parameters included. + +#### Matching on HTTP method + +Use `method` parameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) of the requests to retrieve. + +`method` parameter must be a string. It will be upper-cased, so it can be provided lower cased. + +Matching is performed on equality. + +#### Matching on HTTP headers + +Use `match_headers` parameter to specify the HTTP headers executing the callback. + +Matching is performed on equality for each provided header. + +#### Matching on HTTP body + +Use `match_content` parameter to specify the full HTTP body executing the callback. + +Matching is performed on equality. + +## Do not mock some requests + +By default, `pytest-httpx` will mock every request. + +But, for instance, in case you want to write integration tests with other servers, you might want to let some requests go through. + +To do so, you can use the `non_mocked_hosts` fixture: + +```python +import pytest + +@pytest.fixture +def non_mocked_hosts() -> list: + return ["my_local_test_host", "my_other_test_host"] +``` + +Every other requested hosts will be mocked as in the following example + +```python +import pytest +import httpx + +@pytest.fixture +def non_mocked_hosts() -> list: + return ["my_local_test_host"] + + +def test_partial_mock(httpx_mock): + httpx_mock.add_response() + + with httpx.Client() as client: + # This request will NOT be mocked + response1 = client.get("https://www.my_local_test_host/sub?param=value") + # This request will be mocked + response2 = client.get("https://test_url") +``` + +## Migrating to pytest-httpx + +Here is how to migrate from well-known testing libraries to `pytest-httpx`. + +### From responses + +| Feature | responses | pytest-httpx | +|:------------------|:---------------------------|:----------------------------| +| Add a response | `responses.add()` | `httpx_mock.add_response()` | +| Add a callback | `responses.add_callback()` | `httpx_mock.add_callback()` | +| Retrieve requests | `responses.calls` | `httpx_mock.get_requests()` | + +#### Add a response or a callback + +Undocumented parameters means that they are unchanged between `responses` and `pytest-httpx`. +Below is a list of parameters that will require a change in your code. + +| Parameter | responses | pytest-httpx | +|:---------------------|:------------------------------------|:---------------------------------------------------------------------| +| method | `method=responses.GET` | `method="GET"` | +| body (as bytes) | `body=b"sample"` | `content=b"sample"` | +| body (as str) | `body="sample"` | `text="sample"` | +| status code | `status=201` | `status_code=201` | +| headers | `adding_headers={"name": "value"}` | `headers={"name": "value"}` | +| content-type header | `content_type="application/custom"` | `headers={"content-type": "application/custom"}` | +| Match the full query | `match_querystring=True` | The full query is always matched when providing the `url` parameter. | + +Sample adding a response with `responses`: +```python +from responses import RequestsMock + +def test_response(responses: RequestsMock): + responses.add( + method=responses.GET, + url="https://test_url", + body=b"This is the response content", + status=400, + ) + +``` + +Sample adding the same response with `pytest-httpx`: +```python +from pytest_httpx import HTTPXMock + +def test_response(httpx_mock: HTTPXMock): + httpx_mock.add_response( + method="GET", + url="https://test_url", + content=b"This is the response content", + status_code=400, + ) + +``` + +### From aioresponses + +| Feature | aioresponses | pytest-httpx | +|:---------------|:------------------------|:-------------------------------------------| +| Add a response | `aioresponses.method()` | `httpx_mock.add_response(method="METHOD")` | +| Add a callback | `aioresponses.method()` | `httpx_mock.add_callback(method="METHOD")` | + +#### Add a response or a callback + +Undocumented parameters means that they are unchanged between `responses` and `pytest-httpx`. +Below is a list of parameters that will require a change in your code. + +| Parameter | responses | pytest-httpx | +|:----------------|:---------------------|:--------------------| +| body (as bytes) | `body=b"sample"` | `content=b"sample"` | +| body (as str) | `body="sample"` | `text="sample"` | +| body (as JSON) | `payload=["sample"]` | `json=["sample"]` | +| status code | `status=201` | `status_code=201` | + +Sample adding a response with `aioresponses`: +```python +import pytest +from aioresponses import aioresponses + + +@pytest.fixture +def mock_aioresponse(): + with aioresponses() as m: + yield m + + +def test_response(mock_aioresponse): + mock_aioresponse.get( + url="https://test_url", + body=b"This is the response content", + status=400, + ) + +``` + +Sample adding the same response with `pytest-httpx`: +```python +def test_response(httpx_mock): + httpx_mock.add_response( + method="GET", + url="https://test_url", + content=b"This is the response content", + status_code=400, + ) + +``` + + + + +%package help +Summary: Development documents and examples for pytest-httpx +Provides: python3-pytest-httpx-doc +%description help +<h2 align="center">Send responses to HTTPX using pytest</h2> + +<p align="center"> +<a href="https://pypi.org/project/pytest-httpx/"><img alt="pypi version" src="https://img.shields.io/pypi/v/pytest_httpx"></a> +<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Build status" src="https://github.com/Colin-b/pytest_httpx/workflows/Release/badge.svg"></a> +<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a> +<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a> +<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-168 passed-blue"></a> +<a href="https://pypi.org/project/pytest-httpx/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/pytest_httpx"></a> +</p> + +> Version 1.0.0 will be released once httpx is considered as stable (release of 1.0.0). +> +> However, current state can be considered as stable. + +Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixture will make sure every [`httpx`](https://www.python-httpx.org) request will be replied to with user provided responses. + +- [Add responses](#add-responses) + - [JSON body](#add-json-response) + - [Custom body](#reply-with-custom-body) + - [Multipart body (files, ...)](#add-multipart-response) + - [HTTP status code](#add-non-200-response) + - [HTTP headers](#reply-with-custom-headers) + - [HTTP/2.0](#add-http/2.0-response) +- [Add dynamic responses](#dynamic-responses) +- [Raising exceptions](#raising-exceptions) +- [Check requests](#check-sent-requests) +- [Do not mock some requests](#do-not-mock-some-requests) +- [Migrating](#migrating-to-pytest-httpx) + - [responses](#from-responses) + - [aioresponses](#from-aioresponses) + +## Add responses + +You can register responses for both sync and async [`HTTPX`](https://www.python-httpx.org) requests. + +```python +import pytest +import httpx + + +def test_something(httpx_mock): + httpx_mock.add_response() + + with httpx.Client() as client: + response = client.get("https://test_url") + + +@pytest.mark.asyncio +async def test_something_async(httpx_mock): + httpx_mock.add_response() + + async with httpx.AsyncClient() as client: + response = await client.get("https://test_url") +``` + +If all registered responses are not sent back during test execution, the test case will fail at teardown. + +This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture: + +```python +import pytest + +@pytest.fixture +def assert_all_responses_were_requested() -> bool: + return False +``` + +Default response is a HTTP/1.1 200 (OK) without any body. + +### How response is selected + +In case more than one response match request, the first one not yet sent (according to the registration order) will be sent. + +In case all matching responses have been sent, the last one (according to the registration order) will be sent. + +You can add criteria so that response will be sent only in case of a more specific matching. + +#### Matching on URL + +`url` parameter can either be a string, a python [re.Pattern](https://docs.python.org/3/library/re.html) instance or a [httpx.URL](https://www.python-httpx.org/api/#url) instance. + +Matching is performed on the full URL, query parameters included. + +Order of parameters in the query string does not matter, however order of values do matter if the same parameter is provided more than once. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_url(httpx_mock: HTTPXMock): + httpx_mock.add_response(url="https://test_url?a=1&b=2") + + with httpx.Client() as client: + response1 = client.delete("https://test_url?a=1&b=2") + response2 = client.get("https://test_url?b=2&a=1") +``` + +#### Matching on HTTP method + +Use `method` parameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) to reply to. + +`method` parameter must be a string. It will be upper-cased, so it can be provided lower cased. + +Matching is performed on equality. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_post(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="POST") + + with httpx.Client() as client: + response = client.post("https://test_url") + + +def test_put(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="PUT") + + with httpx.Client() as client: + response = client.put("https://test_url") + + +def test_delete(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="DELETE") + + with httpx.Client() as client: + response = client.delete("https://test_url") + + +def test_patch(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="PATCH") + + with httpx.Client() as client: + response = client.patch("https://test_url") + + +def test_head(httpx_mock: HTTPXMock): + httpx_mock.add_response(method="HEAD") + + with httpx.Client() as client: + response = client.head("https://test_url") + +``` + +#### Matching on HTTP headers + +Use `match_headers` parameter to specify the HTTP headers to reply to. + +Matching is performed on equality for each provided header. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_headers_matching(httpx_mock: HTTPXMock): + httpx_mock.add_response(match_headers={'user-agent': 'python-httpx/0.23.0'}) + + with httpx.Client() as client: + response = client.get("https://test_url") +``` + +#### Matching on HTTP body + +Use `match_content` parameter to specify the full HTTP body to reply to. + +Matching is performed on equality. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_content_matching(httpx_mock: HTTPXMock): + httpx_mock.add_response(match_content=b"This is the body") + + with httpx.Client() as client: + response = client.post("https://test_url", content=b"This is the body") +``` + +### Add JSON response + +Use `json` parameter to add a JSON response using python values. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_json(httpx_mock: HTTPXMock): + httpx_mock.add_response(json=[{"key1": "value1", "key2": "value2"}]) + + with httpx.Client() as client: + assert client.get("https://test_url").json() == [{"key1": "value1", "key2": "value2"}] + +``` + +Note that the `content-type` header will be set to `application/json` by default in the response. + +### Reply with custom body + +Use `text` parameter to reply with a custom body by providing UTF-8 encoded string. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_str_body(httpx_mock: HTTPXMock): + httpx_mock.add_response(text="This is my UTF-8 content") + + with httpx.Client() as client: + assert client.get("https://test_url").text == "This is my UTF-8 content" + +``` + +Use `content` parameter to reply with a custom body by providing bytes. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_bytes_body(httpx_mock: HTTPXMock): + httpx_mock.add_response(content=b"This is my bytes content") + + with httpx.Client() as client: + assert client.get("https://test_url").content == b"This is my bytes content" + +``` + +Use `html` parameter to reply with a custom body by providing UTF-8 encoded string. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_html_body(httpx_mock: HTTPXMock): + httpx_mock.add_response(html="<body>This is <p> HTML content</body>") + + with httpx.Client() as client: + assert client.get("https://test_url").text == "<body>This is <p> HTML content</body>" + +``` + +### Reply by streaming chunks + +Use `stream` parameter to stream chunks that you specify. + +```python +import httpx +import pytest +from pytest_httpx import HTTPXMock, IteratorStream + +def test_sync_streaming(httpx_mock: HTTPXMock): + httpx_mock.add_response(stream=IteratorStream([b"part 1", b"part 2"])) + + with httpx.Client() as client: + with client.stream(method="GET", url="https://test_url") as response: + assert list(response.iter_raw()) == [b"part 1", b"part 2"] + + +@pytest.mark.asyncio +async def test_async_streaming(httpx_mock: HTTPXMock): + httpx_mock.add_response(stream=IteratorStream([b"part 1", b"part 2"])) + + async with httpx.AsyncClient() as client: + async with client.stream(method="GET", url="https://test_url") as response: + assert [part async for part in response.aiter_raw()] == [b"part 1", b"part 2"] + +``` + +### Add multipart response + +Use the httpx `MultipartStream` via the `stream` parameter to send a multipart response. + +Reach out to `httpx` developers if you need this publicly exposed as [this is not a standard use case](https://github.com/encode/httpx/issues/872#issuecomment-633584819). + +```python +import httpx +from httpx._multipart import MultipartStream +from pytest_httpx import HTTPXMock + + +def test_multipart_body(httpx_mock: HTTPXMock): + httpx_mock.add_response(stream=MultipartStream(data={"key1": "value1"}, files={"file1": b"content of file 1"}, boundary=b"2256d3a36d2a61a1eba35a22bee5c74a")) + + with httpx.Client() as client: + assert client.get("https://test_url").text == '''--2256d3a36d2a61a1eba35a22bee5c74a\r +Content-Disposition: form-data; name="key1"\r +\r +value1\r +--2256d3a36d2a61a1eba35a22bee5c74a\r +Content-Disposition: form-data; name="file1"; filename="upload"\r +Content-Type: application/octet-stream\r +\r +content of file 1\r +--2256d3a36d2a61a1eba35a22bee5c74a--\r +''' + +``` + +### Add non 200 response + +Use `status_code` parameter to specify the HTTP status code of the response. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_status_code(httpx_mock: HTTPXMock): + httpx_mock.add_response(status_code=404) + + with httpx.Client() as client: + assert client.get("https://test_url").status_code == 404 + +``` + +### Reply with custom headers + +Use `headers` parameter to specify the extra headers of the response. + +Any valid httpx headers type is supported, you can submit headers as a dict (str or bytes), a list of 2-tuples (str or bytes) or a `httpx.Header` instance. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_headers_as_str_dict(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers={"X-Header1": "Test value"}) + + with httpx.Client() as client: + assert client.get("https://test_url").headers["x-header1"] == "Test value" + + +def test_headers_as_str_tuple_list(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers=[("X-Header1", "Test value")]) + + with httpx.Client() as client: + assert client.get("https://test_url").headers["x-header1"] == "Test value" + + +def test_headers_as_httpx_headers(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers=httpx.Headers({b"X-Header1": b"Test value"})) + + with httpx.Client() as client: + assert client.get("https://test_url").headers["x-header1"] == "Test value" + +``` + +#### Reply with cookies + +Cookies are sent in the `set-cookie` HTTP header. + +You can then send cookies in the response by setting the `set-cookie` header with [the value following key=value format]((https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)). + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_cookie(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers={"set-cookie": "key=value"}) + + with httpx.Client() as client: + response = client.get("https://test_url") + assert dict(response.cookies) == {"key": "value"} + + +def test_cookies(httpx_mock: HTTPXMock): + httpx_mock.add_response(headers=[("set-cookie", "key=value"), ("set-cookie", "key2=value2")]) + + with httpx.Client() as client: + response = client.get("https://test_url") + assert dict(response.cookies) == {"key": "value", "key2": "value2"} + +``` + + +### Add HTTP/2.0 response + +Use `http_version` parameter to specify the HTTP protocol version of the response. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_http_version(httpx_mock: HTTPXMock): + httpx_mock.add_response(http_version="HTTP/2.0") + + with httpx.Client() as client: + assert client.get("https://test_url").http_version == "HTTP/2.0" + +``` + +## Add callbacks + +You can perform custom manipulation upon request reception by registering callbacks. + +Callback should expect one parameter, the received [`httpx.Request`](https://www.python-httpx.org/api/#request). + +If all callbacks are not executed during test execution, the test case will fail at teardown. + +This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture: + +```python +import pytest + +@pytest.fixture +def assert_all_responses_were_requested() -> bool: + return False +``` + +Note that callbacks are considered as responses, and thus are [selected the same way](#how-response-is-selected). + +### Dynamic responses + +Callback should return a `httpx.Response`. + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_dynamic_response(httpx_mock: HTTPXMock): + def custom_response(request: httpx.Request): + return httpx.Response( + status_code=200, json={"url": str(request.url)}, + ) + + httpx_mock.add_callback(custom_response) + + with httpx.Client() as client: + response = client.get("https://test_url") + assert response.json() == {"url": "https://test_url"} + +``` + +Alternatively, callbacks can also be asynchronous. + +As in the following sample simulating network latency on some responses only. + +```python +import asyncio +import httpx +import pytest +from pytest_httpx import HTTPXMock + + +@pytest.mark.asyncio +async def test_dynamic_async_response(httpx_mock: HTTPXMock): + async def simulate_network_latency(request: httpx.Request): + await asyncio.sleep(1) + return httpx.Response( + status_code=200, json={"url": str(request.url)}, + ) + + httpx_mock.add_callback(simulate_network_latency) + httpx_mock.add_response() + + async with httpx.AsyncClient() as client: + responses = await asyncio.gather( + # Response will be received after one second + client.get("https://test_url"), + # Response will instantly be received (1 second before the first request) + client.get("https://test_url") + ) + +``` + +### Raising exceptions + +You can simulate HTTPX exception throwing by raising an exception in your callback or use `httpx_mock.add_exception` with the exception instance. + +This can be useful if you want to assert that your code handles HTTPX exceptions properly. + +```python +import httpx +import pytest +from pytest_httpx import HTTPXMock + + +def test_exception_raising(httpx_mock: HTTPXMock): + httpx_mock.add_exception(httpx.ReadTimeout("Unable to read within timeout")) + + with httpx.Client() as client: + with pytest.raises(httpx.ReadTimeout): + client.get("https://test_url") + +``` + +Note that default behavior is to send an `httpx.TimeoutException` in case no response can be found. You can then test this kind of exception this way: + +```python +import httpx +import pytest +from pytest_httpx import HTTPXMock + + +def test_timeout(httpx_mock: HTTPXMock): + with httpx.Client() as client: + with pytest.raises(httpx.TimeoutException): + client.get("https://test_url") + +``` + +## Check sent requests + +The best way to ensure the content of your requests is still to use the `match_headers` and / or `match_content` parameters when adding a response. +In the same spirit, ensuring that no request was issued does not necessarily requires any code. + +In any case, you always have the ability to retrieve the requests that were issued. + +As in the following samples: + +```python +import httpx +from pytest_httpx import HTTPXMock + + +def test_many_requests(httpx_mock: HTTPXMock): + httpx_mock.add_response() + + with httpx.Client() as client: + response1 = client.get("https://test_url") + response2 = client.get("https://test_url") + + requests = httpx_mock.get_requests() + + +def test_single_request(httpx_mock: HTTPXMock): + httpx_mock.add_response() + + with httpx.Client() as client: + response = client.get("https://test_url") + + request = httpx_mock.get_request() + + +def test_no_request(httpx_mock: HTTPXMock): + assert not httpx_mock.get_request() +``` + +### How requests are selected + +You can add criteria so that requests will be returned only in case of a more specific matching. + +#### Matching on URL + +`url` parameter can either be a string, a python [re.Pattern](https://docs.python.org/3/library/re.html) instance or a [httpx.URL](https://www.python-httpx.org/api/#url) instance. + +Matching is performed on the full URL, query parameters included. + +#### Matching on HTTP method + +Use `method` parameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) of the requests to retrieve. + +`method` parameter must be a string. It will be upper-cased, so it can be provided lower cased. + +Matching is performed on equality. + +#### Matching on HTTP headers + +Use `match_headers` parameter to specify the HTTP headers executing the callback. + +Matching is performed on equality for each provided header. + +#### Matching on HTTP body + +Use `match_content` parameter to specify the full HTTP body executing the callback. + +Matching is performed on equality. + +## Do not mock some requests + +By default, `pytest-httpx` will mock every request. + +But, for instance, in case you want to write integration tests with other servers, you might want to let some requests go through. + +To do so, you can use the `non_mocked_hosts` fixture: + +```python +import pytest + +@pytest.fixture +def non_mocked_hosts() -> list: + return ["my_local_test_host", "my_other_test_host"] +``` + +Every other requested hosts will be mocked as in the following example + +```python +import pytest +import httpx + +@pytest.fixture +def non_mocked_hosts() -> list: + return ["my_local_test_host"] + + +def test_partial_mock(httpx_mock): + httpx_mock.add_response() + + with httpx.Client() as client: + # This request will NOT be mocked + response1 = client.get("https://www.my_local_test_host/sub?param=value") + # This request will be mocked + response2 = client.get("https://test_url") +``` + +## Migrating to pytest-httpx + +Here is how to migrate from well-known testing libraries to `pytest-httpx`. + +### From responses + +| Feature | responses | pytest-httpx | +|:------------------|:---------------------------|:----------------------------| +| Add a response | `responses.add()` | `httpx_mock.add_response()` | +| Add a callback | `responses.add_callback()` | `httpx_mock.add_callback()` | +| Retrieve requests | `responses.calls` | `httpx_mock.get_requests()` | + +#### Add a response or a callback + +Undocumented parameters means that they are unchanged between `responses` and `pytest-httpx`. +Below is a list of parameters that will require a change in your code. + +| Parameter | responses | pytest-httpx | +|:---------------------|:------------------------------------|:---------------------------------------------------------------------| +| method | `method=responses.GET` | `method="GET"` | +| body (as bytes) | `body=b"sample"` | `content=b"sample"` | +| body (as str) | `body="sample"` | `text="sample"` | +| status code | `status=201` | `status_code=201` | +| headers | `adding_headers={"name": "value"}` | `headers={"name": "value"}` | +| content-type header | `content_type="application/custom"` | `headers={"content-type": "application/custom"}` | +| Match the full query | `match_querystring=True` | The full query is always matched when providing the `url` parameter. | + +Sample adding a response with `responses`: +```python +from responses import RequestsMock + +def test_response(responses: RequestsMock): + responses.add( + method=responses.GET, + url="https://test_url", + body=b"This is the response content", + status=400, + ) + +``` + +Sample adding the same response with `pytest-httpx`: +```python +from pytest_httpx import HTTPXMock + +def test_response(httpx_mock: HTTPXMock): + httpx_mock.add_response( + method="GET", + url="https://test_url", + content=b"This is the response content", + status_code=400, + ) + +``` + +### From aioresponses + +| Feature | aioresponses | pytest-httpx | +|:---------------|:------------------------|:-------------------------------------------| +| Add a response | `aioresponses.method()` | `httpx_mock.add_response(method="METHOD")` | +| Add a callback | `aioresponses.method()` | `httpx_mock.add_callback(method="METHOD")` | + +#### Add a response or a callback + +Undocumented parameters means that they are unchanged between `responses` and `pytest-httpx`. +Below is a list of parameters that will require a change in your code. + +| Parameter | responses | pytest-httpx | +|:----------------|:---------------------|:--------------------| +| body (as bytes) | `body=b"sample"` | `content=b"sample"` | +| body (as str) | `body="sample"` | `text="sample"` | +| body (as JSON) | `payload=["sample"]` | `json=["sample"]` | +| status code | `status=201` | `status_code=201` | + +Sample adding a response with `aioresponses`: +```python +import pytest +from aioresponses import aioresponses + + +@pytest.fixture +def mock_aioresponse(): + with aioresponses() as m: + yield m + + +def test_response(mock_aioresponse): + mock_aioresponse.get( + url="https://test_url", + body=b"This is the response content", + status=400, + ) + +``` + +Sample adding the same response with `pytest-httpx`: +```python +def test_response(httpx_mock): + httpx_mock.add_response( + method="GET", + url="https://test_url", + content=b"This is the response content", + status_code=400, + ) + +``` + + + + +%prep +%autosetup -n pytest-httpx-0.21.3 + +%build +%py3_build + +%install +%py3_install +install -d -m755 %{buildroot}/%{_pkgdocdir} +if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi +if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi +if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi +if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi +pushd %{buildroot} +if [ -d usr/lib ]; then + find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/lib64 ]; then + find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/bin ]; then + find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/sbin ]; then + find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst +fi +touch doclist.lst +if [ -d usr/share/man ]; then + find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst +fi +popd +mv %{buildroot}/filelist.lst . +mv %{buildroot}/doclist.lst . + +%files -n python3-pytest-httpx -f filelist.lst +%dir %{python3_sitelib}/* + +%files help -f doclist.lst +%{_docdir}/* + +%changelog +* Thu Mar 09 2023 Python_Bot <Python_Bot@openeuler.org> - 0.21.3-1 +- Package Spec generated |