From b386c52ef08b1b79bdaf632adb5eb1f6dd16350e Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Wed, 25 Sep 2024 22:16:32 +0200
Subject: [PATCH 01/17] Enhance assertion failure messages
---
CHANGELOG.md | 9 ++++++
README.md | 4 +--
pytest_httpx/_httpx_mock.py | 28 ++++++++++++-----
tests/test_httpx_async.py | 60 ++++++++++++++++++-------------------
tests/test_httpx_sync.py | 60 ++++++++++++++++++-------------------
tests/test_plugin.py | 23 ++++++++++----
6 files changed, 108 insertions(+), 76 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dcbbf35..e4dfa72 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Added
+- Assertion failure message in case of unmatched responses is now linking documentation on how to deactivate the check.
+- Assertion failure message in case of unmatched requests is now linking documentation on how to deactivate the check.
+
+### Fixed
+- Assertion failure message in case of unmatched requests at teardown is now describing requests in a more user-friendly way.
+- Assertion failure message in case of unmatched requests at teardown is now prefixing requests with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
+- Assertion failure message in case of unmatched responses at teardown is now prefixing responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
+- TimeoutException message issued in case of unmatched request is now prefixing available responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
## [0.31.2] - 2024-09-23
### Fixed
diff --git a/README.md b/README.md
index d119519..6b37a36 100644
--- a/README.md
+++ b/README.md
@@ -639,7 +639,7 @@ Refer to [available options](#available-options) for an exhaustive list of optio
```python
import pytest
-@pytest.mark.httpx_mock(assert_all_responses_were_requested=True)
+@pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
def test_something(httpx_mock):
...
```
@@ -676,7 +676,7 @@ This option can be useful if you add responses using shared fixtures.
```python
import pytest
-@pytest.mark.httpx_mock(assert_all_responses_were_requested=True)
+@pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
def test_fewer_requests_than_expected(httpx_mock):
# Even if this response never received a corresponding request, the test will not fail at teardown
httpx_mock.add_response()
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 3379e2b..74e81cc 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -202,7 +202,7 @@ def _explain_that_no_response_was_found(
message = f"No response can be found for {RequestDescription(real_transport, request, matchers)}"
- matchers_description = "\n".join([str(matcher) for matcher in matchers])
+ matchers_description = "\n".join([f"- {matcher}" for matcher in matchers])
if matchers_description:
message += f" amongst:\n{matchers_description}"
@@ -290,17 +290,29 @@ def _assert_options(self, options: HTTPXMockOptions) -> None:
matcher for matcher, _ in self._callbacks if not matcher.nb_calls
]
matchers_description = "\n".join(
- [str(matcher) for matcher in callbacks_not_executed]
+ [f"- {matcher}" for matcher in callbacks_not_executed]
)
- assert (
- not callbacks_not_executed
- ), f"The following responses are mocked but not requested:\n{matchers_description}"
+ assert not callbacks_not_executed, (
+ "The following responses are mocked but not requested:\n"
+ f"{matchers_description}\n"
+ "\n"
+ "If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested"
+ )
if options.assert_all_requests_were_expected:
- assert (
- not self._requests_not_matched
- ), f"The following requests were not expected:\n{self._requests_not_matched}"
+ requests_description = "\n".join(
+ [
+ f"- {request.method} request on {request.url}"
+ for request in self._requests_not_matched
+ ]
+ )
+ assert not self._requests_not_matched, (
+ f"The following requests were not expected:\n"
+ f"{requests_description}\n"
+ "\n"
+ "If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request"
+ )
def _unread(response: httpx.Response) -> httpx.Response:
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index ee3cc61..b05576a 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -75,7 +75,7 @@ async def test_url_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url2 amongst:
-Match all requests on https://test_url"""
+- Match all requests on https://test_url"""
)
@@ -93,7 +93,7 @@ async def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url?a=2&a=1 amongst:
-Match all requests on https://test_url?a=1&a=2"""
+- Match all requests on https://test_url?a=1&a=2"""
)
@@ -122,7 +122,7 @@ async def test_method_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url amongst:
-Match GET requests"""
+- Match GET requests"""
)
@@ -1129,7 +1129,7 @@ async def test_multi_value_headers_not_matching_single_value_issued(
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value2'} headers amongst:
-Match all requests with {'my-custom-header': 'value1'} headers"""
+- Match all requests with {'my-custom-header': 'value1'} headers"""
)
@@ -1154,7 +1154,7 @@ async def test_multi_value_headers_not_matching_multi_value_issued(
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value3'} headers amongst:
-Match all requests with {'my-custom-header': 'value1, value2'} headers"""
+- Match all requests with {'my-custom-header': 'value1, value2'} headers"""
)
@@ -1173,7 +1173,7 @@ async def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
-Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers"""
+- Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers"""
)
@@ -1196,7 +1196,7 @@ async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
-Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers"""
+- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers"""
)
@@ -1218,7 +1218,7 @@ async def test_url_not_matching_upper_case_headers_matching(
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst:
-Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
+- Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
)
@@ -1253,7 +1253,7 @@ async def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on http://test_url with http://my_test_proxy/ proxy URL amongst:
-Match all requests with http://my_test_proxy proxy URL"""
+- Match all requests with http://my_test_proxy proxy URL"""
)
@@ -1270,7 +1270,7 @@ async def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on http://test_url with no proxy URL amongst:
-Match all requests with http://my_test_proxy proxy URL"""
+- Match all requests with http://my_test_proxy proxy URL"""
)
@@ -1351,7 +1351,7 @@ async def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'This is the body2' body amongst:
-Match all requests with b'This is the body' body"""
+- Match all requests with b'This is the body' body"""
)
@@ -1386,7 +1386,7 @@ async def test_json_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'{"c": 3, "b": 2, "a": 1}' body amongst:
-Match all requests with {'a': 1, 'b': 2} json body"""
+- Match all requests with {'a': 1, 'b': 2} json body"""
)
@@ -1406,7 +1406,7 @@ async def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with {} headers and b'{"c": 3, "b": 2, "a": 1}' body amongst:
-Match all requests with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body"""
+- Match all requests with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body"""
)
@@ -1423,7 +1423,7 @@ async def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'foobar' body amongst:
-Match all requests with {'a': 1, 'b': 2} json body"""
+- Match all requests with {'a': 1, 'b': 2} json body"""
)
@@ -1458,7 +1458,7 @@ async def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock)
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1481,7 +1481,7 @@ async def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock)
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1504,7 +1504,7 @@ async def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1543,7 +1543,7 @@ async def test_headers_not_matching_and_url_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1569,7 +1569,7 @@ async def test_url_and_headers_not_matching_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1595,7 +1595,7 @@ async def test_url_and_headers_matching_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1621,7 +1621,7 @@ async def test_headers_matching_and_url_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1647,7 +1647,7 @@ async def test_url_matching_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1671,7 +1671,7 @@ async def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1714,7 +1714,7 @@ async def test_headers_not_matching_and_method_and_url_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1741,7 +1741,7 @@ async def test_url_and_headers_not_matching_and_method_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1768,7 +1768,7 @@ async def test_method_and_url_and_headers_matching_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1795,7 +1795,7 @@ async def test_method_and_headers_matching_and_url_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1822,7 +1822,7 @@ async def test_method_and_url_matching_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1849,7 +1849,7 @@ async def test_method_matching_and_url_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1876,7 +1876,7 @@ async def test_method_and_url_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match PUT requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match PUT requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index a4c6047..f37bdb8 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -67,7 +67,7 @@ def test_url_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url2 amongst:
-Match all requests on https://test_url"""
+- Match all requests on https://test_url"""
)
@@ -84,7 +84,7 @@ def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url?a=2&a=1 amongst:
-Match all requests on https://test_url?a=1&a=2"""
+- Match all requests on https://test_url?a=1&a=2"""
)
@@ -111,7 +111,7 @@ def test_method_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url amongst:
-Match GET requests"""
+- Match GET requests"""
)
@@ -157,7 +157,7 @@ def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) ->
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst:
-Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
+- Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
)
@@ -902,7 +902,7 @@ def test_multi_value_headers_not_matching_single_value_issued(
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value2'} headers amongst:
-Match all requests with {'my-custom-header': 'value1'} headers"""
+- Match all requests with {'my-custom-header': 'value1'} headers"""
)
@@ -926,7 +926,7 @@ def test_multi_value_headers_not_matching_multi_value_issued(
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value3'} headers amongst:
-Match all requests with {'my-custom-header': 'value1, value2'} headers"""
+- Match all requests with {'my-custom-header': 'value1, value2'} headers"""
)
@@ -944,7 +944,7 @@ def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
-Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers"""
+- Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers"""
)
@@ -966,7 +966,7 @@ def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
-Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers"""
+- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers"""
)
@@ -998,7 +998,7 @@ def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on http://test_url with http://my_test_proxy/ proxy URL amongst:
-Match all requests with http://my_test_proxy proxy URL"""
+- Match all requests with http://my_test_proxy proxy URL"""
)
@@ -1014,7 +1014,7 @@ def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on http://test_url with no proxy URL amongst:
-Match all requests with http://my_test_proxy proxy URL"""
+- Match all requests with http://my_test_proxy proxy URL"""
)
@@ -1086,7 +1086,7 @@ def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'This is the body2' body amongst:
-Match all requests with b'This is the body' body"""
+- Match all requests with b'This is the body' body"""
)
@@ -1128,7 +1128,7 @@ def test_json_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'{"c": 3, "b": 2, "a": 1}' body amongst:
-Match all requests with {'a': 1, 'b': 2} json body"""
+- Match all requests with {'a': 1, 'b': 2} json body"""
)
@@ -1147,7 +1147,7 @@ def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with {} headers and b'{"c": 3, "b": 2, "a": 1}' body amongst:
-Match all requests with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body"""
+- Match all requests with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body"""
)
@@ -1163,7 +1163,7 @@ def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'foobar' body amongst:
-Match all requests with {'a': 1, 'b': 2} json body"""
+- Match all requests with {'a': 1, 'b': 2} json body"""
)
@@ -1196,7 +1196,7 @@ def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> Non
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1218,7 +1218,7 @@ def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> Non
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1240,7 +1240,7 @@ def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1277,7 +1277,7 @@ def test_headers_not_matching_and_url_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1302,7 +1302,7 @@ def test_url_and_headers_not_matching_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1327,7 +1327,7 @@ def test_url_and_headers_matching_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1352,7 +1352,7 @@ def test_headers_matching_and_url_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1377,7 +1377,7 @@ def test_url_matching_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1400,7 +1400,7 @@ def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1439,7 +1439,7 @@ def test_headers_not_matching_and_method_and_url_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1465,7 +1465,7 @@ def test_url_and_headers_not_matching_and_method_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1491,7 +1491,7 @@ def test_method_and_url_and_headers_matching_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1517,7 +1517,7 @@ def test_method_and_headers_matching_and_url_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1543,7 +1543,7 @@ def test_method_and_url_matching_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1569,7 +1569,7 @@ def test_method_matching_and_url_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1595,7 +1595,7 @@ def test_method_and_url_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match PUT requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match PUT requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index a3149a0..faa5fa1 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -1,3 +1,5 @@
+import re
+
from pytest import Testdir
@@ -36,8 +38,11 @@ def test_httpx_mock_unused_response(httpx_mock):
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
- "*Match all requests",
- ]
+ "* - Match all requests",
+ "* ",
+ "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
+ ],
+ consecutive=True,
)
@@ -78,8 +83,11 @@ def unused(*args, **kwargs):
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
- "*Match all requests",
- ]
+ "* - Match all requests",
+ "* ",
+ "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
+ ],
+ consecutive=True,
)
@@ -127,8 +135,11 @@ def test_httpx_mock_unexpected_request(httpx_mock):
result.stdout.fnmatch_lines(
[
"*AssertionError: The following requests were not expected:",
- "*[]",
- ]
+ "* - GET request on https://foo.tld",
+ "* ",
+ "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request",
+ ],
+ consecutive=True,
)
From 58cc134d14ab73f5e854794257ac213b63f564d8 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Wed, 25 Sep 2024 22:22:18 +0200
Subject: [PATCH 02/17] Remove unused imports
---
pytest_httpx/_httpx_internals.py | 7 +------
tests/test_plugin.py | 2 --
2 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/pytest_httpx/_httpx_internals.py b/pytest_httpx/_httpx_internals.py
index 66199ff..477d2bc 100644
--- a/pytest_httpx/_httpx_internals.py
+++ b/pytest_httpx/_httpx_internals.py
@@ -1,10 +1,5 @@
import base64
-from typing import (
- Union,
- Dict,
- Tuple,
- Optional,
-)
+from typing import Union, Optional
from collections.abc import Sequence, Iterable, AsyncIterator, Iterator
import httpcore
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index faa5fa1..d0b06e9 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -1,5 +1,3 @@
-import re
-
from pytest import Testdir
From 1b3e2b5a3eff3787bd880fe69f2461af15d1bbe1 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Thu, 26 Sep 2024 22:41:12 +0200
Subject: [PATCH 03/17] Clarify what is public and what is not
---
CHANGELOG.md | 7 +++++++
pytest_httpx/__init__.py | 8 ++++----
pytest_httpx/_httpx_mock.py | 15 ++++++++++-----
3 files changed, 21 insertions(+), 9 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4dfa72..da5e939 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Assertion failure message in case of unmatched responses at teardown is now prefixing responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
- TimeoutException message issued in case of unmatched request is now prefixing available responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
+### Changed
+- `HTTPXMock` class was only exposed for type hinting purpose. This is now explained in the class docstring.
+ - As a result this is the last time a change to `__init__` signature will be documented and considered a breaking change.
+ - Future changes will not be documented and will be considered as internal refactoring not worth a version bump.
+ - `__init__` now expects one parameter, the newly introduced (since [0.31.0]) options.
+- `HTTPXMockOptions` class was never intended to be exposed and is now marked as private.
+
## [0.31.2] - 2024-09-23
### Fixed
- `httpx_mock` marker can now be defined at different levels for a single test.
diff --git a/pytest_httpx/__init__.py b/pytest_httpx/__init__.py
index 402dd54..fe1f9de 100644
--- a/pytest_httpx/__init__.py
+++ b/pytest_httpx/__init__.py
@@ -5,7 +5,7 @@
import pytest
from pytest import Config, FixtureRequest, MonkeyPatch
-from pytest_httpx._httpx_mock import HTTPXMock, HTTPXMockOptions
+from pytest_httpx._httpx_mock import HTTPXMock, _HTTPXMockOptions
from pytest_httpx._httpx_internals import IteratorStream
from pytest_httpx.version import __version__
@@ -25,9 +25,9 @@ def httpx_mock(
for marker in request.node.iter_markers("httpx_mock"):
options = marker.kwargs | options
__tracebackhide__ = methodcaller("errisinstance", TypeError)
- options = HTTPXMockOptions(**options)
+ options = _HTTPXMockOptions(**options)
- mock = HTTPXMock()
+ mock = HTTPXMock(options)
# Mock synchronous requests
real_handle_request = httpx.HTTPTransport.handle_request
@@ -63,7 +63,7 @@ async def mocked_handle_async_request(
yield mock
try:
- mock._assert_options(options)
+ mock._assert_options()
finally:
mock.reset()
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 74e81cc..28e9321 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -10,7 +10,7 @@
from pytest_httpx._request_matcher import _RequestMatcher
-class HTTPXMockOptions:
+class _HTTPXMockOptions:
def __init__(
self,
*,
@@ -32,7 +32,12 @@ def __init__(
class HTTPXMock:
- def __init__(self) -> None:
+ """
+ This class is only exposed for `httpx_mock` type hinting purpose.
+ """
+
+ def __init__(self, options: _HTTPXMockOptions) -> None:
+ self._options = options
self._requests: list[
tuple[Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport], httpx.Request]
] = []
@@ -284,8 +289,8 @@ def reset(self) -> None:
self._callbacks.clear()
self._requests_not_matched.clear()
- def _assert_options(self, options: HTTPXMockOptions) -> None:
- if options.assert_all_responses_were_requested:
+ def _assert_options(self) -> None:
+ if self._options.assert_all_responses_were_requested:
callbacks_not_executed = [
matcher for matcher, _ in self._callbacks if not matcher.nb_calls
]
@@ -300,7 +305,7 @@ def _assert_options(self, options: HTTPXMockOptions) -> None:
"If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested"
)
- if options.assert_all_requests_were_expected:
+ if self._options.assert_all_requests_were_expected:
requests_description = "\n".join(
[
f"- {request.method} request on {request.url}"
From e3922ab9b973f9b1665a315ec0bca3b27a7b7a57 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Thu, 26 Sep 2024 22:44:29 +0200
Subject: [PATCH 04/17] Clarify what is public and what is not
---
pytest_httpx/_httpx_mock.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 28e9321..96c3520 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -33,10 +33,11 @@ def __init__(
class HTTPXMock:
"""
- This class is only exposed for `httpx_mock` type hinting purpose.
+ This class is only exposed for `httpx_mock` fixture type hinting purpose.
"""
def __init__(self, options: _HTTPXMockOptions) -> None:
+ """Private and subject to breaking changes without notice."""
self._options = options
self._requests: list[
tuple[Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport], httpx.Request]
From f663eee4657c849fc3fadbfce5e3ade9caca8ffc Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 27 Sep 2024 00:43:07 +0200
Subject: [PATCH 05/17] Already matched responses are not sent by default
anymore
---
CHANGELOG.md | 9 +++-
README.md | 93 ++++++++++++++++++-------------------
pytest_httpx/__init__.py | 2 +-
pytest_httpx/_httpx_mock.py | 12 +++--
tests/test_httpx_async.py | 57 ++++++++++++++++++++++-
tests/test_httpx_sync.py | 51 +++++++++++++++++++-
tests/test_plugin.py | 55 ++++++++++++++++++++++
7 files changed, 223 insertions(+), 56 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da5e939..55cdc0c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,16 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
+- The following option is now available:
+ - `can_send_already_matched_responses` (boolean), defaulting to `False`.
- Assertion failure message in case of unmatched responses is now linking documentation on how to deactivate the check.
- Assertion failure message in case of unmatched requests is now linking documentation on how to deactivate the check.
### Fixed
+- Documentation now clearly state the risks associated with changing the default options.
- Assertion failure message in case of unmatched requests at teardown is now describing requests in a more user-friendly way.
- Assertion failure message in case of unmatched requests at teardown is now prefixing requests with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
- Assertion failure message in case of unmatched responses at teardown is now prefixing responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
- TimeoutException message issued in case of unmatched request is now prefixing available responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
### Changed
+- Last registered matching response will not be reused by default anymore in case all matching responses have already been sent.
+ - This behavior can be changed thanks to the new `pytest.mark.httpx_mock(can_send_already_matched_responses=True)` option.
+ - The incentive behind this change is to spot regression if a request was issued more than the expected number of times.
- `HTTPXMock` class was only exposed for type hinting purpose. This is now explained in the class docstring.
- As a result this is the last time a change to `__init__` signature will be documented and considered a breaking change.
- Future changes will not be documented and will be considered as internal refactoring not worth a version bump.
@@ -33,7 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.31.0] - 2024-09-20
### Changed
- Tests will now fail at teardown by default if some requests were issued but were not matched.
- - This behavior can be changed thanks to the new ``pytest.mark.httpx_mock(assert_all_requests_were_expected=False)`` option.
+ - This behavior can be changed thanks to the new `pytest.mark.httpx_mock(assert_all_requests_were_expected=False)` option.
+ - The incentive behind this change is to spot unexpected requests in case code is swallowing `httpx.TimeoutException`.
- The `httpx_mock` fixture is now configured using a marker (many thanks to [`Frazer McLean`](https://github.com/RazerM)).
```python
# Apply marker to whole module
diff --git a/README.md b/README.md
index 6b37a36..f8d1745 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@
+> [!NOTE]
> 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.
@@ -28,6 +29,7 @@ Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixt
- [Configuration](#configuring-httpx_mock)
- [Register more responses than requested](#allow-to-register-more-responses-than-what-will-be-requested)
- [Register less responses than requested](#allow-to-not-register-responses-for-every-request)
+ - [Allow to register a response for more than one request](#allow-to-register-a-response-for-more-than-one-request)
- [Do not mock some requests](#do-not-mock-some-requests)
- [Migrating](#migrating-to-pytest-httpx)
- [responses](#from-responses)
@@ -59,13 +61,13 @@ async def test_something_async(httpx_mock):
If all registered responses are not sent back during test execution, the test case will fail at teardown [(unless you turned `assert_all_responses_were_requested` option off)](#allow-to-register-more-responses-than-what-will-be-requested).
-Default response is a HTTP/1.1 200 (OK) without any body.
+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.
+In case all matching responses have been sent once, the request will [not be considered as matched](#in-case-no-response-can-be-found) [(unless you turned `can_send_already_matched_responses` option on)](#allow-to-register-a-response-for-more-than-one-request).
You can add criteria so that response will be sent only in case of a more specific matching.
@@ -366,7 +368,7 @@ def test_status_code(httpx_mock: HTTPXMock):
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.
+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`](https://www.python-httpx.org/api/#headers) instance.
```python
import httpx
@@ -450,10 +452,11 @@ Callback should expect one parameter, the received [`httpx.Request`](https://www
If all callbacks are not executed during test execution, the test case will fail at teardown [(unless you turned `assert_all_responses_were_requested` option off)](#allow-to-register-more-responses-than-what-will-be-requested).
Note that callbacks are considered as responses, and thus are [selected the same way](#how-response-is-selected).
+Meaning that you can transpose `httpx_mock.add_response` calls in the related examples into `httpx_mock.add_callback`.
### Dynamic responses
-Callback should return a `httpx.Response`.
+Callback should return a [`httpx.Response`](https://www.python-httpx.org/api/#response) instance.
```python
import httpx
@@ -527,7 +530,11 @@ def test_exception_raising(httpx_mock: HTTPXMock):
```
-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:
+#### In case no response can be found
+
+The default behavior is to instantly raise a [`httpx.TimeoutException`](https://www.python-httpx.org/advanced/timeouts/) in case no matching response can be found.
+
+The exception message will display the request and every registered responses to help you identify any possible mismatch.
```python
import httpx
@@ -584,49 +591,8 @@ def test_no_request(httpx_mock: HTTPXMock):
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.
-
-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.
-
-#### 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 proxy URL
-
-`proxy_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 proxy 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.
-
-#### 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.
-
-##### Matching on HTTP JSON body
-
-Use `match_json` parameter to specify the JSON decoded HTTP body executing the callback.
-
-Matching is performed on equality. You can however use `unittest.mock.ANY` to do partial matching.
-
-Note that `match_content` cannot be provided if `match_json` is also provided.
+Note that requests are [selected the same way as responses](#how-response-is-selected).
+Meaning that you can transpose `httpx_mock.add_response` calls in the related examples into `httpx_mock.get_requests` or `httpx_mock.get_request`.
## Configuring httpx_mock
@@ -673,6 +639,9 @@ You can use the `httpx_mock` marker `assert_all_responses_were_requested` option
This option can be useful if you add responses using shared fixtures.
+> [!CAUTION]
+> Use this option at your own risk of not spotting regression (requests not sent) in your code base!
+
```python
import pytest
@@ -687,7 +656,9 @@ def test_fewer_requests_than_expected(httpx_mock):
By default, `pytest-httpx` will ensure that every request that was issued was expected.
You can use the `httpx_mock` marker `assert_all_requests_were_expected` option to allow more requests than what you registered responses for.
-Use this option at your own risk of not spotting regression in your code base!
+
+> [!CAUTION]
+> Use this option at your own risk of not spotting regression (unexpected requests) in your code base!
```python
import pytest
@@ -701,6 +672,30 @@ def test_more_requests_than_expected(httpx_mock):
client.get("https://test_url")
```
+#### Allow to register a response for more than one request
+
+By default, `pytest-httpx` will ensure that every request that was issued was expected.
+
+You can use the `httpx_mock` marker `can_send_already_matched_responses` option to allow multiple requests to match the same registered response.
+
+With this option, in case all matching responses have been sent at least once, the last one (according to the registration order) will be sent.
+
+> [!CAUTION]
+> Use this option at your own risk of not spotting regression (requests issued more than the expected number of times) in your code base!
+
+```python
+import pytest
+import httpx
+
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
+def test_more_requests_than_responses(httpx_mock):
+ httpx_mock.add_response()
+ with httpx.Client() as client:
+ client.get("https://test_url")
+ # Even if only one response was registered, the test will not fail at teardown as this request will also be matched
+ client.get("https://test_url")
+```
+
#### Do not mock some requests
By default, `pytest-httpx` will mock every request.
diff --git a/pytest_httpx/__init__.py b/pytest_httpx/__init__.py
index fe1f9de..66b2469 100644
--- a/pytest_httpx/__init__.py
+++ b/pytest_httpx/__init__.py
@@ -71,5 +71,5 @@ async def mocked_handle_async_request(
def pytest_configure(config: Config) -> None:
config.addinivalue_line(
"markers",
- "httpx_mock(*, assert_all_responses_were_requested=True, assert_all_requests_were_expected=True, non_mocked_hosts=[]): Configure httpx_mock fixture.",
+ "httpx_mock(*, assert_all_responses_were_requested=True, assert_all_requests_were_expected=True, can_send_already_matched_responses=False, non_mocked_hosts=[]): Configure httpx_mock fixture.",
)
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 96c3520..55657d7 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -16,10 +16,12 @@ def __init__(
*,
assert_all_responses_were_requested: bool = True,
assert_all_requests_were_expected: bool = True,
+ can_send_already_matched_responses: bool = False,
non_mocked_hosts: Optional[list[str]] = None,
) -> None:
self.assert_all_responses_were_requested = assert_all_responses_were_requested
self.assert_all_requests_were_expected = assert_all_requests_were_expected
+ self.can_send_already_matched_responses = can_send_already_matched_responses
if non_mocked_hosts is None:
non_mocked_hosts = []
@@ -241,9 +243,13 @@ def _get_callback(
matcher.nb_calls += 1
return callback
- # Or the last registered
- matcher.nb_calls += 1
- return callback
+ # Or the last registered (if it can be reused)
+ if self._options.can_send_already_matched_responses:
+ matcher.nb_calls += 1
+ return callback
+
+ # All callbacks have already been matched and last registered cannot be reused
+ return None
def get_requests(self, **matchers: Any) -> list[httpx.Request]:
"""
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index b05576a..ab5890c 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -45,11 +45,22 @@ async def test_url_matching(httpx_mock: HTTPXMock) -> None:
response = await client.get("https://test_url")
assert response.content == b""
+
+@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
+async def test_url_matching_reusing_response(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(url="https://test_url")
+
+ async with httpx.AsyncClient() as client:
+ response = await client.get("https://test_url")
+ assert response.content == b""
+
response = await client.post("https://test_url")
assert response.content == b""
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url?a=1&b=2")
@@ -98,6 +109,7 @@ async def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_method_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(method="get")
@@ -127,7 +139,8 @@ async def test_method_not_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
-async def test_with_one_response(httpx_mock: HTTPXMock) -> None:
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
+async def test_reusing_one_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", content=b"test content")
async with httpx.AsyncClient() as client:
@@ -157,6 +170,7 @@ async def test_response_with_html_string_body(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
@@ -186,6 +200,7 @@ async def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_content_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
@@ -213,6 +228,7 @@ async def test_content_response_streaming(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_text_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
@@ -240,6 +256,7 @@ async def test_text_response_streaming(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_default_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -263,6 +280,24 @@ async def test_default_response_streaming(httpx_mock: HTTPXMock) -> None:
async def test_with_many_responses(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", content=b"test content 1")
httpx_mock.add_response(url="https://test_url", content=b"test content 2")
+ httpx_mock.add_response(url="https://test_url", content=b"test content 2")
+
+ async with httpx.AsyncClient() as client:
+ response = await client.get("https://test_url")
+ assert response.content == b"test content 1"
+
+ response = await client.get("https://test_url")
+ assert response.content == b"test content 2"
+
+ response = await client.get("https://test_url")
+ assert response.content == b"test content 2"
+
+
+@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
+async def test_with_many_reused_responses(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(url="https://test_url", content=b"test content 1")
+ httpx_mock.add_response(url="https://test_url", content=b"test content 2")
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
@@ -694,6 +729,7 @@ async def test_requests_retrieval(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_requests_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url")
@@ -708,6 +744,7 @@ async def test_requests_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_request_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -720,6 +757,7 @@ async def test_request_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_requests_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -734,6 +772,7 @@ async def test_requests_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_request_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -746,6 +785,7 @@ async def test_request_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_requests_retrieval_on_same_url_and_method(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -762,6 +802,7 @@ async def test_requests_retrieval_on_same_url_and_method(httpx_mock: HTTPXMock)
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_default_requests_retrieval(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -896,6 +937,7 @@ async def custom_response(request: httpx.Request) -> httpx.Response:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_callback_executed_twice(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content"])
@@ -913,6 +955,7 @@ def custom_response(request: httpx.Request) -> httpx.Response:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_async_callback_executed_twice(httpx_mock: HTTPXMock) -> None:
async def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content"])
@@ -930,6 +973,7 @@ async def custom_response(request: httpx.Request) -> httpx.Response:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_callback_registered_after_response(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content2"])
@@ -953,6 +997,7 @@ def custom_response(request: httpx.Request) -> httpx.Response:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_async_callback_registered_after_response(httpx_mock: HTTPXMock) -> None:
async def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content2"])
@@ -976,6 +1021,7 @@ async def custom_response(request: httpx.Request) -> httpx.Response:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_response_registered_after_callback(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content1"])
@@ -999,6 +1045,7 @@ def custom_response(request: httpx.Request) -> httpx.Response:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_response_registered_after_async_callback(httpx_mock: HTTPXMock) -> None:
async def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content1"])
@@ -1022,6 +1069,7 @@ async def custom_response(request: httpx.Request) -> httpx.Response:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_callback_matching_method(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content"])
@@ -1039,6 +1087,7 @@ def custom_response(request: httpx.Request) -> httpx.Response:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_async_callback_matching_method(httpx_mock: HTTPXMock) -> None:
async def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content"])
@@ -1066,6 +1115,7 @@ def test_request_retrieval_with_more_than_one(testdir: Testdir) -> None:
@pytest.mark.asyncio
+ @pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_request_retrieval_with_more_than_one(httpx_mock):
httpx_mock.add_response()
@@ -1275,6 +1325,7 @@ async def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -1287,6 +1338,7 @@ async def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> Non
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_requests_retrieval_json_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -1299,6 +1351,7 @@ async def test_requests_retrieval_json_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_requests_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -1320,6 +1373,7 @@ async def test_requests_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_request_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -2003,6 +2057,7 @@ async def test_mutating_json(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
async def test_streams_are_not_cascading_resulting_in_maximum_recursion(
httpx_mock: HTTPXMock,
) -> None:
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index f37bdb8..e1a9be7 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -39,10 +39,20 @@ def test_url_matching(httpx_mock: HTTPXMock) -> None:
response = client.get("https://test_url")
assert response.content == b""
+
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
+def test_url_matching_reusing_response(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(url="https://test_url")
+
+ with httpx.Client() as client:
+ response = client.get("https://test_url")
+ assert response.content == b""
+
response = client.post("https://test_url")
assert response.content == b""
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url?a=1&b=2")
@@ -88,6 +98,7 @@ def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None:
)
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_method_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(method="get")
@@ -115,7 +126,8 @@ def test_method_not_matching(httpx_mock: HTTPXMock) -> None:
)
-def test_with_one_response(httpx_mock: HTTPXMock) -> None:
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
+def test_reusing_one_response(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", content=b"test content")
with httpx.Client() as client:
@@ -161,6 +173,7 @@ def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) ->
)
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
@@ -182,6 +195,7 @@ def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None:
list(response.iter_raw())
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_content_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
@@ -203,6 +217,7 @@ def test_content_response_streaming(httpx_mock: HTTPXMock) -> None:
list(response.iter_raw())
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_text_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
@@ -224,6 +239,7 @@ def test_text_response_streaming(httpx_mock: HTTPXMock) -> None:
list(response.iter_raw())
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_default_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -245,6 +261,23 @@ def test_default_response_streaming(httpx_mock: HTTPXMock) -> None:
def test_with_many_responses(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", content=b"test content 1")
httpx_mock.add_response(url="https://test_url", content=b"test content 2")
+ httpx_mock.add_response(url="https://test_url", content=b"test content 2")
+
+ with httpx.Client() as client:
+ response = client.get("https://test_url")
+ assert response.content == b"test content 1"
+
+ response = client.get("https://test_url")
+ assert response.content == b"test content 2"
+
+ response = client.get("https://test_url")
+ assert response.content == b"test content 2"
+
+
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
+def test_with_many_reused_responses(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(url="https://test_url", content=b"test content 1")
+ httpx_mock.add_response(url="https://test_url", content=b"test content 2")
with httpx.Client() as client:
response = client.get("https://test_url")
@@ -596,6 +629,7 @@ def test_requests_retrieval(httpx_mock: HTTPXMock) -> None:
)
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_requests_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url")
@@ -609,6 +643,7 @@ def test_requests_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None:
assert requests[1].headers["x-test"] == "test header 2"
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_request_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -620,6 +655,7 @@ def test_request_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None:
assert request.headers["x-test"] == "test header 1"
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_requests_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -633,6 +669,7 @@ def test_requests_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None:
assert requests[1].headers["x-test"] == "test header 2"
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_request_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -644,6 +681,7 @@ def test_request_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None:
assert request.headers["x-test"] == "test header 1"
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_requests_retrieval_on_same_url_and_method(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -659,6 +697,7 @@ def test_requests_retrieval_on_same_url_and_method(httpx_mock: HTTPXMock) -> Non
assert requests[1].headers["x-test"] == "test header 2"
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_default_requests_retrieval(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -757,6 +796,7 @@ def custom_response(request: httpx.Request) -> httpx.Response:
assert response.headers["content-type"] == "application/json"
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_callback_executed_twice(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content"])
@@ -773,6 +813,7 @@ def custom_response(request: httpx.Request) -> httpx.Response:
assert response.headers["content-type"] == "application/json"
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_callback_registered_after_response(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content2"])
@@ -795,6 +836,7 @@ def custom_response(request: httpx.Request) -> httpx.Response:
assert response.headers["content-type"] == "application/json"
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_response_registered_after_callback(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content1"])
@@ -817,6 +859,7 @@ def custom_response(request: httpx.Request) -> httpx.Response:
assert response.headers["content-type"] == "application/json"
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_callback_matching_method(httpx_mock: HTTPXMock) -> None:
def custom_response(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=200, json=["content"])
@@ -840,8 +883,10 @@ def test_request_retrieval_with_more_than_one(testdir: Testdir) -> None:
testdir.makepyfile(
"""
import httpx
+ import pytest
+ @pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_request_retrieval_with_more_than_one(httpx_mock):
httpx_mock.add_response()
@@ -1018,6 +1063,7 @@ def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None:
)
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -1029,6 +1075,7 @@ def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None:
assert len(httpx_mock.get_requests(match_content=b"This is the body")) == 2
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_requests_retrieval_json_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -1040,6 +1087,7 @@ def test_requests_retrieval_json_matching(httpx_mock: HTTPXMock) -> None:
assert len(httpx_mock.get_requests(match_json=["my_str"])) == 2
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_requests_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
@@ -1058,6 +1106,7 @@ def test_requests_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None:
)
+@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_request_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response()
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index d0b06e9..30e6fe6 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -163,6 +163,61 @@ def test_httpx_mock_unexpected_request(httpx_mock):
result.assert_outcomes(passed=1)
+def test_httpx_mock_already_matched_response(testdir: Testdir) -> None:
+ """
+ Already matched response should fail test case if
+ can_send_already_matched_responses option is set to False (default).
+ """
+ testdir.makepyfile(
+ """
+ import httpx
+ import pytest
+
+ def test_httpx_mock_already_matched_response(httpx_mock):
+ httpx_mock.add_response()
+ with httpx.Client() as client:
+ client.get("https://foo.tld")
+ # Non mocked (already matched) request
+ with pytest.raises(httpx.TimeoutException):
+ client.get("https://foo.tld")
+ """
+ )
+ result = testdir.runpytest()
+ result.assert_outcomes(errors=1, passed=1)
+ result.stdout.fnmatch_lines(
+ [
+ "*AssertionError: The following requests were not expected:",
+ "* - GET request on https://foo.tld",
+ "* ",
+ "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request",
+ ],
+ consecutive=True,
+ )
+
+
+def test_httpx_mock_reusing_matched_response(testdir: Testdir) -> None:
+ """
+ Already matched response should not fail test case if
+ can_send_already_matched_responses option is set to True.
+ """
+ testdir.makepyfile(
+ """
+ import httpx
+ import pytest
+
+ @pytest.mark.httpx_mock(can_send_already_matched_responses=True)
+ def test_httpx_mock_reusing_matched_response(httpx_mock):
+ httpx_mock.add_response()
+ with httpx.Client() as client:
+ client.get("https://foo.tld")
+ # Reusing response
+ client.get("https://foo.tld")
+ """
+ )
+ result = testdir.runpytest()
+ result.assert_outcomes(passed=1)
+
+
def test_httpx_mock_non_mocked_hosts_sync(testdir: Testdir) -> None:
"""
Non mocked hosts should go through while other requests should be mocked.
From e5e4d05e60e0491953024ded468c02e9ac399de6 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 27 Sep 2024 00:46:02 +0200
Subject: [PATCH 06/17] Keep number of tests up to date
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index f8d1745..4009d71 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
From fe3d039b52cbffb90a7a7a909f182566f427bbb6 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 27 Sep 2024 08:58:13 +0200
Subject: [PATCH 07/17] Document current pytest behavior
---
README.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/README.md b/README.md
index 4009d71..9360669 100644
--- a/README.md
+++ b/README.md
@@ -629,6 +629,15 @@ def pytest_collection_modifyitems(session, config, items):
item.add_marker(pytest.mark.httpx_mock(assert_all_responses_were_requested=False))
```
+> [!IMPORTANT]
+> Note that [there currently is a bug in pytest](https://github.com/pytest-dev/pytest/issues/10406) where `pytest_collection_modifyitems` will actually add the marker AFTER its `module` and `class` registration.
+>
+> Meaning the order is currently:
+> module -> class -> test suite -> test
+>
+> instead of:
+> test suite -> module -> class -> test
+
### Available options
#### Allow to register more responses than what will be requested
From c1b82a21ca37c719ba1913538d3d045fa45fc49c Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 27 Sep 2024 09:43:41 +0200
Subject: [PATCH 08/17] Enhance timeoutexception message
---
CHANGELOG.md | 4 ++-
README.md | 2 +-
pytest_httpx/_httpx_mock.py | 10 ++++++-
tests/test_plugin.py | 53 +++++++++++++++++++++++++++++++++++++
4 files changed, 66 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 55cdc0c..df746b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,7 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Assertion failure message in case of unmatched requests at teardown is now describing requests in a more user-friendly way.
- Assertion failure message in case of unmatched requests at teardown is now prefixing requests with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
- Assertion failure message in case of unmatched responses at teardown is now prefixing responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
-- TimeoutException message issued in case of unmatched request is now prefixing available responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
+- `httpx.TimeoutException` message issued in case of unmatched request is now prefixing available responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
+- `httpx.TimeoutException` message issued in case of unmatched request is now listing unmatched responses (in registration order) before already matched one (still in registration order).
+ - The incentive behind this change is to help identify a potential mismatch faster as the first unmatched response is the most likely to be the one expected to match.
### Changed
- Last registered matching response will not be reused by default anymore in case all matching responses have already been sent.
diff --git a/README.md b/README.md
index 9360669..4f227ac 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 55657d7..07f5f02 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -210,7 +210,15 @@ def _explain_that_no_response_was_found(
message = f"No response can be found for {RequestDescription(real_transport, request, matchers)}"
- matchers_description = "\n".join([f"- {matcher}" for matcher in matchers])
+ unmatched = []
+ for matcher in matchers:
+ if not matcher.nb_calls:
+ matchers.remove(matcher)
+ unmatched.append(matcher)
+
+ matchers_description = "\n".join(
+ [f"- {matcher}" for matcher in unmatched + matchers]
+ )
if matchers_description:
message += f" amongst:\n{matchers_description}"
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 30e6fe6..18bee11 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -218,6 +218,59 @@ def test_httpx_mock_reusing_matched_response(httpx_mock):
result.assert_outcomes(passed=1)
+def test_httpx_mock_unmatched_request_with_matched_and_unmatched_response(
+ testdir: Testdir,
+) -> None:
+ testdir.makepyfile(
+ """
+ import httpx
+ import pytest
+
+ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_response(httpx_mock):
+ # Sent response
+ httpx_mock.add_response(url="https://foo.tld")
+ # This response will not be sent (because of a typo in the URL)
+ httpx_mock.add_response(url="https://foo2.tld")
+ # Sent response
+ httpx_mock.add_response(url="https://foo.tld")
+ # This response will not be sent (because test execution failed earlier)
+ httpx_mock.add_response(url="https://foo3.tld")
+
+ with httpx.Client() as client:
+ client.get("https://foo.tld")
+ client.get("https://foo.tld")
+ # This request will not be matched
+ client.get("https://foo22.tld")
+ # This code will not be reached
+ client.get("https://foo3.tld")
+ """
+ )
+ result = testdir.runpytest()
+ result.assert_outcomes(errors=1, failed=1)
+ # Assert the error that occurred
+ result.stdout.fnmatch_lines(
+ [
+ "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
+ "*- Match all requests on https://foo2.tld",
+ "*- Match all requests on https://foo3.tld",
+ "*- Match all requests on https://foo.tld",
+ "*- Match all requests on https://foo.tld",
+ ],
+ consecutive=True,
+ )
+ # Assert the teardown assertion failure
+ result.stdout.fnmatch_lines(
+ [
+ "*AssertionError: The following responses are mocked but not requested:",
+ "* - Match all requests on https://foo2.tld",
+ "* - Match all requests on https://foo3.tld",
+ "* ",
+ "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
+ ],
+ consecutive=True,
+ )
+
+
def test_httpx_mock_non_mocked_hosts_sync(testdir: Testdir) -> None:
"""
Non mocked hosts should go through while other requests should be mocked.
From ef2bcef1973f150574ccfaf8f464f9fcb0712a89 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 27 Sep 2024 09:54:23 +0200
Subject: [PATCH 09/17] Test all possible combination of unmatched request
---
README.md | 2 +-
tests/test_plugin.py | 132 ++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 131 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 4f227ac..0d3fa54 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 18bee11..8aa626c 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -218,7 +218,7 @@ def test_httpx_mock_reusing_matched_response(httpx_mock):
result.assert_outcomes(passed=1)
-def test_httpx_mock_unmatched_request_with_matched_and_unmatched_response(
+def test_httpx_mock_unmatched_request_without_responses(
testdir: Testdir,
) -> None:
testdir.makepyfile(
@@ -226,7 +226,135 @@ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_response(
import httpx
import pytest
- def test_httpx_mock_unmatched_request_with_matched_and_unmatched_response(httpx_mock):
+ def test_httpx_mock_unmatched_request_without_responses(httpx_mock):
+ with httpx.Client() as client:
+ # This request will not be matched
+ client.get("https://foo22.tld")
+ # This code will not be reached
+ client.get("https://foo3.tld")
+ """
+ )
+ result = testdir.runpytest()
+ result.assert_outcomes(errors=1, failed=1)
+ # Assert the error that occurred
+ result.stdout.fnmatch_lines(
+ [
+ "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld",
+ ],
+ consecutive=True,
+ )
+ # Assert the teardown assertion failure
+ result.stdout.fnmatch_lines(
+ [
+ "*AssertionError: The following requests were not expected:",
+ "* - GET request on https://foo22.tld",
+ "* ",
+ "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request",
+ ],
+ consecutive=True,
+ )
+
+
+def test_httpx_mock_unmatched_request_with_only_unmatched_responses(
+ testdir: Testdir,
+) -> None:
+ testdir.makepyfile(
+ """
+ import httpx
+ import pytest
+
+ def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock):
+ # This response will not be sent (because of a typo in the URL)
+ httpx_mock.add_response(url="https://foo2.tld")
+ # This response will not be sent (because test execution failed earlier)
+ httpx_mock.add_response(url="https://foo3.tld")
+
+ with httpx.Client() as client:
+ # This request will not be matched
+ client.get("https://foo22.tld")
+ # This code will not be reached
+ client.get("https://foo3.tld")
+ """
+ )
+ result = testdir.runpytest()
+ result.assert_outcomes(errors=1, failed=1)
+ # Assert the error that occurred
+ result.stdout.fnmatch_lines(
+ [
+ "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
+ "*- Match all requests on https://foo2.tld",
+ "*- Match all requests on https://foo3.tld",
+ ],
+ consecutive=True,
+ )
+ # Assert the teardown assertion failure
+ result.stdout.fnmatch_lines(
+ [
+ "*AssertionError: The following responses are mocked but not requested:",
+ "* - Match all requests on https://foo2.tld",
+ "* - Match all requests on https://foo3.tld",
+ "* ",
+ "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
+ ],
+ consecutive=True,
+ )
+
+
+def test_httpx_mock_unmatched_request_with_only_matched_responses(
+ testdir: Testdir,
+) -> None:
+ testdir.makepyfile(
+ """
+ import httpx
+ import pytest
+
+ def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock):
+ # Sent response
+ httpx_mock.add_response(url="https://foo.tld")
+ # Sent response
+ httpx_mock.add_response(url="https://foo.tld")
+
+ with httpx.Client() as client:
+ client.get("https://foo.tld")
+ client.get("https://foo.tld")
+ # This request will not be matched
+ client.get("https://foo22.tld")
+ # This code will not be reached
+ client.get("https://foo3.tld")
+ """
+ )
+ result = testdir.runpytest()
+ result.assert_outcomes(errors=1, failed=1)
+ # Assert the error that occurred
+ result.stdout.fnmatch_lines(
+ [
+ "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
+ "*- Match all requests on https://foo.tld",
+ "*- Match all requests on https://foo.tld",
+ ],
+ consecutive=True,
+ )
+ # Assert the teardown assertion failure
+ result.stdout.fnmatch_lines(
+ [
+ "*AssertionError: The following requests were not expected:",
+ "* - GET request on https://foo22.tld",
+ "* ",
+ "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request",
+ ],
+ consecutive=True,
+ )
+
+
+def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(
+ testdir: Testdir,
+) -> None:
+ testdir.makepyfile(
+ """
+ import httpx
+ import pytest
+
+ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx_mock):
# Sent response
httpx_mock.add_response(url="https://foo.tld")
# This response will not be sent (because of a typo in the URL)
From 356026bf6eba538576058382575263dd54b741f4 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 27 Sep 2024 10:16:13 +0200
Subject: [PATCH 10/17] Cleanup failure description
---
CHANGELOG.md | 1 +
pytest_httpx/_request_matcher.py | 3 +-
tests/test_httpx_async.py | 60 ++++++++++++++++----------------
tests/test_httpx_sync.py | 60 ++++++++++++++++----------------
tests/test_plugin.py | 28 +++++++--------
5 files changed, 77 insertions(+), 75 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index df746b8..f9a6043 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `httpx.TimeoutException` message issued in case of unmatched request is now prefixing available responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists.
- `httpx.TimeoutException` message issued in case of unmatched request is now listing unmatched responses (in registration order) before already matched one (still in registration order).
- The incentive behind this change is to help identify a potential mismatch faster as the first unmatched response is the most likely to be the one expected to match.
+- Response description in failure messages (`httpx.TimeoutException` message issued in case of unmatched request or assertion failure message in case of unmatched responses at teardown) is now displaying if the response was already matched or not and less misleading in it's phrasing about what it can match (a single request by default).
### Changed
- Last registered matching response will not be reused by default anymore in case all matching responses have already been sent.
diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py
index 1143ca3..0572d4a 100644
--- a/pytest_httpx/_request_matcher.py
+++ b/pytest_httpx/_request_matcher.py
@@ -119,7 +119,8 @@ def _proxy_match(
return False
def __str__(self) -> str:
- matcher_description = f"Match {self.method or 'all'} requests"
+ matcher_description = "Already matched" if self.nb_calls else "Match"
+ matcher_description += f" {self.method or 'any'} request"
if self.url:
matcher_description += f" on {self.url}"
if extra_description := self._extra_description():
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index ab5890c..c304ca7 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -86,7 +86,7 @@ async def test_url_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url2 amongst:
-- Match all requests on https://test_url"""
+- Match any request on https://test_url"""
)
@@ -104,7 +104,7 @@ async def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url?a=2&a=1 amongst:
-- Match all requests on https://test_url?a=1&a=2"""
+- Match any request on https://test_url?a=1&a=2"""
)
@@ -134,7 +134,7 @@ async def test_method_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url amongst:
-- Match GET requests"""
+- Match GET request"""
)
@@ -1179,7 +1179,7 @@ async def test_multi_value_headers_not_matching_single_value_issued(
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value2'} headers amongst:
-- Match all requests with {'my-custom-header': 'value1'} headers"""
+- Match any request with {'my-custom-header': 'value1'} headers"""
)
@@ -1204,7 +1204,7 @@ async def test_multi_value_headers_not_matching_multi_value_issued(
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value3'} headers amongst:
-- Match all requests with {'my-custom-header': 'value1, value2'} headers"""
+- Match any request with {'my-custom-header': 'value1, value2'} headers"""
)
@@ -1223,7 +1223,7 @@ async def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
-- Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers"""
+- Match any request with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers"""
)
@@ -1246,7 +1246,7 @@ async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
-- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers"""
+- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers"""
)
@@ -1268,7 +1268,7 @@ async def test_url_not_matching_upper_case_headers_matching(
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst:
-- Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
+- Match GET request on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
)
@@ -1303,7 +1303,7 @@ async def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on http://test_url with http://my_test_proxy/ proxy URL amongst:
-- Match all requests with http://my_test_proxy proxy URL"""
+- Match any request with http://my_test_proxy proxy URL"""
)
@@ -1320,7 +1320,7 @@ async def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on http://test_url with no proxy URL amongst:
-- Match all requests with http://my_test_proxy proxy URL"""
+- Match any request with http://my_test_proxy proxy URL"""
)
@@ -1405,7 +1405,7 @@ async def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'This is the body2' body amongst:
-- Match all requests with b'This is the body' body"""
+- Match any request with b'This is the body' body"""
)
@@ -1440,7 +1440,7 @@ async def test_json_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'{"c": 3, "b": 2, "a": 1}' body amongst:
-- Match all requests with {'a': 1, 'b': 2} json body"""
+- Match any request with {'a': 1, 'b': 2} json body"""
)
@@ -1460,7 +1460,7 @@ async def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with {} headers and b'{"c": 3, "b": 2, "a": 1}' body amongst:
-- Match all requests with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body"""
+- Match any request with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body"""
)
@@ -1477,7 +1477,7 @@ async def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'foobar' body amongst:
-- Match all requests with {'a': 1, 'b': 2} json body"""
+- Match any request with {'a': 1, 'b': 2} json body"""
)
@@ -1512,7 +1512,7 @@ async def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock)
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1535,7 +1535,7 @@ async def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock)
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1558,7 +1558,7 @@ async def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1597,7 +1597,7 @@ async def test_headers_not_matching_and_url_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1623,7 +1623,7 @@ async def test_url_and_headers_not_matching_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1649,7 +1649,7 @@ async def test_url_and_headers_matching_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1675,7 +1675,7 @@ async def test_headers_matching_and_url_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1701,7 +1701,7 @@ async def test_url_matching_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1725,7 +1725,7 @@ async def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1768,7 +1768,7 @@ async def test_headers_not_matching_and_method_and_url_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1795,7 +1795,7 @@ async def test_url_and_headers_not_matching_and_method_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1822,7 +1822,7 @@ async def test_method_and_url_and_headers_matching_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1849,7 +1849,7 @@ async def test_method_and_headers_matching_and_url_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1876,7 +1876,7 @@ async def test_method_and_url_matching_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1903,7 +1903,7 @@ async def test_method_matching_and_url_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1930,7 +1930,7 @@ async def test_method_and_url_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match PUT requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match PUT request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index e1a9be7..9449e7e 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -77,7 +77,7 @@ def test_url_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url2 amongst:
-- Match all requests on https://test_url"""
+- Match any request on https://test_url"""
)
@@ -94,7 +94,7 @@ def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url?a=2&a=1 amongst:
-- Match all requests on https://test_url?a=1&a=2"""
+- Match any request on https://test_url?a=1&a=2"""
)
@@ -122,7 +122,7 @@ def test_method_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url amongst:
-- Match GET requests"""
+- Match GET request"""
)
@@ -169,7 +169,7 @@ def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) ->
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst:
-- Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
+- Match GET request on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
)
@@ -947,7 +947,7 @@ def test_multi_value_headers_not_matching_single_value_issued(
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value2'} headers amongst:
-- Match all requests with {'my-custom-header': 'value1'} headers"""
+- Match any request with {'my-custom-header': 'value1'} headers"""
)
@@ -971,7 +971,7 @@ def test_multi_value_headers_not_matching_multi_value_issued(
assert (
str(exception_info.value)
== """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value3'} headers amongst:
-- Match all requests with {'my-custom-header': 'value1, value2'} headers"""
+- Match any request with {'my-custom-header': 'value1, value2'} headers"""
)
@@ -989,7 +989,7 @@ def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
-- Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers"""
+- Match any request with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers"""
)
@@ -1011,7 +1011,7 @@ def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
-- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers"""
+- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers"""
)
@@ -1043,7 +1043,7 @@ def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on http://test_url with http://my_test_proxy/ proxy URL amongst:
-- Match all requests with http://my_test_proxy proxy URL"""
+- Match any request with http://my_test_proxy proxy URL"""
)
@@ -1059,7 +1059,7 @@ def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for GET request on http://test_url with no proxy URL amongst:
-- Match all requests with http://my_test_proxy proxy URL"""
+- Match any request with http://my_test_proxy proxy URL"""
)
@@ -1135,7 +1135,7 @@ def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'This is the body2' body amongst:
-- Match all requests with b'This is the body' body"""
+- Match any request with b'This is the body' body"""
)
@@ -1177,7 +1177,7 @@ def test_json_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'{"c": 3, "b": 2, "a": 1}' body amongst:
-- Match all requests with {'a': 1, 'b': 2} json body"""
+- Match any request with {'a': 1, 'b': 2} json body"""
)
@@ -1196,7 +1196,7 @@ def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with {} headers and b'{"c": 3, "b": 2, "a": 1}' body amongst:
-- Match all requests with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body"""
+- Match any request with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body"""
)
@@ -1212,7 +1212,7 @@ def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== """No response can be found for POST request on https://test_url with b'foobar' body amongst:
-- Match all requests with {'a': 1, 'b': 2} json body"""
+- Match any request with {'a': 1, 'b': 2} json body"""
)
@@ -1245,7 +1245,7 @@ def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> Non
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1267,7 +1267,7 @@ def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> Non
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1289,7 +1289,7 @@ def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1326,7 +1326,7 @@ def test_headers_not_matching_and_url_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1351,7 +1351,7 @@ def test_url_and_headers_not_matching_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1376,7 +1376,7 @@ def test_url_and_headers_matching_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1401,7 +1401,7 @@ def test_headers_matching_and_url_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1426,7 +1426,7 @@ def test_url_matching_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1449,7 +1449,7 @@ def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1488,7 +1488,7 @@ def test_headers_not_matching_and_method_and_url_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1514,7 +1514,7 @@ def test_url_and_headers_not_matching_and_method_and_content_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
+- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
@@ -1540,7 +1540,7 @@ def test_method_and_url_and_headers_matching_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1566,7 +1566,7 @@ def test_method_and_headers_matching_and_url_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
+- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
@@ -1592,7 +1592,7 @@ def test_method_and_url_matching_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1618,7 +1618,7 @@ def test_method_matching_and_url_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
@@ -1644,7 +1644,7 @@ def test_method_and_url_and_headers_and_content_not_matching(
assert (
str(exception_info.value)
== f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-- Match PUT requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
+- Match PUT request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 8aa626c..d28824d 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -36,7 +36,7 @@ def test_httpx_mock_unused_response(httpx_mock):
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
- "* - Match all requests",
+ "* - Match any request",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
],
@@ -81,7 +81,7 @@ def unused(*args, **kwargs):
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
- "* - Match all requests",
+ "* - Match any request",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
],
@@ -282,8 +282,8 @@ def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock):
result.stdout.fnmatch_lines(
[
"*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
- "*- Match all requests on https://foo2.tld",
- "*- Match all requests on https://foo3.tld",
+ "*- Match any request on https://foo2.tld",
+ "*- Match any request on https://foo3.tld",
],
consecutive=True,
)
@@ -291,8 +291,8 @@ def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock):
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
- "* - Match all requests on https://foo2.tld",
- "* - Match all requests on https://foo3.tld",
+ "* - Match any request on https://foo2.tld",
+ "* - Match any request on https://foo3.tld",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
],
@@ -329,8 +329,8 @@ def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock):
result.stdout.fnmatch_lines(
[
"*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
- "*- Match all requests on https://foo.tld",
- "*- Match all requests on https://foo.tld",
+ "*- Already matched any request on https://foo.tld",
+ "*- Already matched any request on https://foo.tld",
],
consecutive=True,
)
@@ -379,10 +379,10 @@ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx
result.stdout.fnmatch_lines(
[
"*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
- "*- Match all requests on https://foo2.tld",
- "*- Match all requests on https://foo3.tld",
- "*- Match all requests on https://foo.tld",
- "*- Match all requests on https://foo.tld",
+ "*- Match any request on https://foo2.tld",
+ "*- Match any request on https://foo3.tld",
+ "*- Already matched any request on https://foo.tld",
+ "*- Already matched any request on https://foo.tld",
],
consecutive=True,
)
@@ -390,8 +390,8 @@ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
- "* - Match all requests on https://foo2.tld",
- "* - Match all requests on https://foo3.tld",
+ "* - Match any request on https://foo2.tld",
+ "* - Match any request on https://foo3.tld",
"* ",
"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
],
From d9a9b8615fae06c0b82c87e10b06911eceabae0b Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 27 Sep 2024 12:08:51 +0200
Subject: [PATCH 11/17] Cleanup failure description for
can_send_already_matched_responses users as well
---
pytest_httpx/__init__.py | 3 +-
pytest_httpx/_httpx_mock.py | 28 +-----
pytest_httpx/_options.py | 24 +++++
pytest_httpx/_request_matcher.py | 10 ++-
tests/test_plugin.py | 149 +++++++++++++++++++++++++++++++
5 files changed, 186 insertions(+), 28 deletions(-)
create mode 100644 pytest_httpx/_options.py
diff --git a/pytest_httpx/__init__.py b/pytest_httpx/__init__.py
index 66b2469..2066f2e 100644
--- a/pytest_httpx/__init__.py
+++ b/pytest_httpx/__init__.py
@@ -5,8 +5,9 @@
import pytest
from pytest import Config, FixtureRequest, MonkeyPatch
-from pytest_httpx._httpx_mock import HTTPXMock, _HTTPXMockOptions
+from pytest_httpx._httpx_mock import HTTPXMock
from pytest_httpx._httpx_internals import IteratorStream
+from pytest_httpx._options import _HTTPXMockOptions
from pytest_httpx.version import __version__
__all__ = (
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 07f5f02..bf45b7a 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -6,33 +6,11 @@
import httpx
from pytest_httpx import _httpx_internals
+from pytest_httpx._options import _HTTPXMockOptions
from pytest_httpx._pretty_print import RequestDescription
from pytest_httpx._request_matcher import _RequestMatcher
-class _HTTPXMockOptions:
- def __init__(
- self,
- *,
- assert_all_responses_were_requested: bool = True,
- assert_all_requests_were_expected: bool = True,
- can_send_already_matched_responses: bool = False,
- non_mocked_hosts: Optional[list[str]] = None,
- ) -> None:
- self.assert_all_responses_were_requested = assert_all_responses_were_requested
- self.assert_all_requests_were_expected = assert_all_requests_were_expected
- self.can_send_already_matched_responses = can_send_already_matched_responses
-
- if non_mocked_hosts is None:
- non_mocked_hosts = []
-
- # Ensure redirections to www hosts are handled transparently.
- missing_www = [
- f"www.{host}" for host in non_mocked_hosts if not host.startswith("www.")
- ]
- self.non_mocked_hosts = [*non_mocked_hosts, *missing_www]
-
-
class HTTPXMock:
"""
This class is only exposed for `httpx_mock` fixture type hinting purpose.
@@ -128,7 +106,7 @@ def add_callback(
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
:param match_json: JSON decoded HTTP body identifying the request(s) to match. Must be JSON encodable.
"""
- self._callbacks.append((_RequestMatcher(**matchers), callback))
+ self._callbacks.append((_RequestMatcher(self._options, **matchers), callback))
def add_exception(self, exception: Exception, **matchers: Any) -> None:
"""
@@ -272,7 +250,7 @@ def get_requests(self, **matchers: Any) -> list[httpx.Request]:
:param match_content: Full HTTP body identifying the requests to retrieve. Must be bytes.
:param match_json: JSON decoded HTTP body identifying the requests to retrieve. Must be JSON encodable.
"""
- matcher = _RequestMatcher(**matchers)
+ matcher = _RequestMatcher(self._options, **matchers)
return [
request
for real_transport, request in self._requests
diff --git a/pytest_httpx/_options.py b/pytest_httpx/_options.py
new file mode 100644
index 0000000..a76d613
--- /dev/null
+++ b/pytest_httpx/_options.py
@@ -0,0 +1,24 @@
+from typing import Optional
+
+
+class _HTTPXMockOptions:
+ def __init__(
+ self,
+ *,
+ assert_all_responses_were_requested: bool = True,
+ assert_all_requests_were_expected: bool = True,
+ can_send_already_matched_responses: bool = False,
+ non_mocked_hosts: Optional[list[str]] = None,
+ ) -> None:
+ self.assert_all_responses_were_requested = assert_all_responses_were_requested
+ self.assert_all_requests_were_expected = assert_all_requests_were_expected
+ self.can_send_already_matched_responses = can_send_already_matched_responses
+
+ if non_mocked_hosts is None:
+ non_mocked_hosts = []
+
+ # Ensure redirections to www hosts are handled transparently.
+ missing_www = [
+ f"www.{host}" for host in non_mocked_hosts if not host.startswith("www.")
+ ]
+ self.non_mocked_hosts = [*non_mocked_hosts, *missing_www]
diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py
index 0572d4a..7ccc9b9 100644
--- a/pytest_httpx/_request_matcher.py
+++ b/pytest_httpx/_request_matcher.py
@@ -6,6 +6,7 @@
import httpx
from pytest_httpx._httpx_internals import _proxy_url
+from pytest_httpx._options import _HTTPXMockOptions
def _url_match(
@@ -28,6 +29,7 @@ def _url_match(
class _RequestMatcher:
def __init__(
self,
+ options: _HTTPXMockOptions,
url: Optional[Union[str, Pattern[str], httpx.URL]] = None,
method: Optional[str] = None,
proxy_url: Optional[Union[str, Pattern[str], httpx.URL]] = None,
@@ -35,6 +37,7 @@ def __init__(
match_content: Optional[bytes] = None,
match_json: Optional[Any] = None,
):
+ self._options = options
self.nb_calls = 0
self.url = httpx.URL(url) if url and isinstance(url, str) else url
self.method = method.upper() if method else method
@@ -119,8 +122,11 @@ def _proxy_match(
return False
def __str__(self) -> str:
- matcher_description = "Already matched" if self.nb_calls else "Match"
- matcher_description += f" {self.method or 'any'} request"
+ if self._options.can_send_already_matched_responses:
+ matcher_description = f"Match {self.method or 'every'} request"
+ else:
+ matcher_description = "Already matched" if self.nb_calls else "Match"
+ matcher_description += f" {self.method or 'any'} request"
if self.url:
matcher_description += f" on {self.url}"
if extra_description := self._extra_description():
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index d28824d..d2c2cc9 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -300,6 +300,52 @@ def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock):
)
+def test_httpx_mock_unmatched_request_with_only_unmatched_reusable_responses(
+ testdir: Testdir,
+) -> None:
+ testdir.makepyfile(
+ """
+ import httpx
+ import pytest
+
+ @pytest.mark.httpx_mock(can_send_already_matched_responses=True)
+ def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock):
+ # This response will not be sent (because of a typo in the URL)
+ httpx_mock.add_response(url="https://foo2.tld", method="GET")
+ # This response will not be sent (because test execution failed earlier)
+ httpx_mock.add_response(url="https://foo3.tld")
+
+ with httpx.Client() as client:
+ # This request will not be matched
+ client.get("https://foo22.tld")
+ # This code will not be reached
+ client.get("https://foo3.tld")
+ """
+ )
+ result = testdir.runpytest()
+ result.assert_outcomes(errors=1, failed=1)
+ # Assert the error that occurred
+ result.stdout.fnmatch_lines(
+ [
+ "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
+ "*- Match GET request on https://foo2.tld",
+ "*- Match every request on https://foo3.tld",
+ ],
+ consecutive=True,
+ )
+ # Assert the teardown assertion failure
+ result.stdout.fnmatch_lines(
+ [
+ "*AssertionError: The following responses are mocked but not requested:",
+ "* - Match GET request on https://foo2.tld",
+ "* - Match every request on https://foo3.tld",
+ "* ",
+ "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
+ ],
+ consecutive=True,
+ )
+
+
def test_httpx_mock_unmatched_request_with_only_matched_responses(
testdir: Testdir,
) -> None:
@@ -346,6 +392,54 @@ def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock):
)
+def test_httpx_mock_unmatched_request_with_only_matched_reusable_responses(
+ testdir: Testdir,
+) -> None:
+ testdir.makepyfile(
+ """
+ import httpx
+ import pytest
+
+ @pytest.mark.httpx_mock(can_send_already_matched_responses=True)
+ def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock):
+ # Sent response
+ httpx_mock.add_response(url="https://foo.tld")
+ # Sent response
+ httpx_mock.add_response(url="https://foo3.tld")
+
+ with httpx.Client() as client:
+ client.get("https://foo.tld")
+ client.get("https://foo.tld")
+ client.get("https://foo3.tld")
+ # This request will not be matched
+ client.get("https://foo22.tld")
+ # This code will not be reached
+ client.get("https://foo3.tld")
+ """
+ )
+ result = testdir.runpytest()
+ result.assert_outcomes(errors=1, failed=1)
+ # Assert the error that occurred
+ result.stdout.fnmatch_lines(
+ [
+ "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
+ "*- Match every request on https://foo.tld",
+ "*- Match every request on https://foo3.tld",
+ ],
+ consecutive=True,
+ )
+ # Assert the teardown assertion failure
+ result.stdout.fnmatch_lines(
+ [
+ "*AssertionError: The following requests were not expected:",
+ "* - GET request on https://foo22.tld",
+ "* ",
+ "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request",
+ ],
+ consecutive=True,
+ )
+
+
def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(
testdir: Testdir,
) -> None:
@@ -399,6 +493,61 @@ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx
)
+def test_httpx_mock_unmatched_request_with_matched_and_unmatched_reusable_responses(
+ testdir: Testdir,
+) -> None:
+ testdir.makepyfile(
+ """
+ import httpx
+ import pytest
+
+ @pytest.mark.httpx_mock(can_send_already_matched_responses=True)
+ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx_mock):
+ # Sent response
+ httpx_mock.add_response(url="https://foo.tld")
+ # This response will not be sent (because of a typo in the URL)
+ httpx_mock.add_response(url="https://foo33.tld")
+ # Sent response
+ httpx_mock.add_response(url="https://foo2.tld")
+ # This response will not be sent (because test execution failed earlier)
+ httpx_mock.add_response(url="https://foo4.tld")
+
+ with httpx.Client() as client:
+ client.get("https://foo.tld")
+ client.get("https://foo2.tld")
+ client.get("https://foo.tld")
+ # This request will not be matched
+ client.get("https://foo3.tld")
+ # This code will not be reached
+ client.get("https://foo2.tld")
+ """
+ )
+ result = testdir.runpytest()
+ result.assert_outcomes(errors=1, failed=1)
+ # Assert the error that occurred
+ result.stdout.fnmatch_lines(
+ [
+ "*httpx.TimeoutException: No response can be found for GET request on https://foo3.tld amongst:",
+ "*- Match every request on https://foo33.tld",
+ "*- Match every request on https://foo4.tld",
+ "*- Match every request on https://foo.tld",
+ "*- Match every request on https://foo2.tld",
+ ],
+ consecutive=True,
+ )
+ # Assert the teardown assertion failure
+ result.stdout.fnmatch_lines(
+ [
+ "*AssertionError: The following responses are mocked but not requested:",
+ "* - Match every request on https://foo33.tld",
+ "* - Match every request on https://foo4.tld",
+ "* ",
+ "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested",
+ ],
+ consecutive=True,
+ )
+
+
def test_httpx_mock_non_mocked_hosts_sync(testdir: Testdir) -> None:
"""
Non mocked hosts should go through while other requests should be mocked.
From 853c5f7909e36a09c2806e48ba38a1f3b883d765 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 27 Sep 2024 12:15:17 +0200
Subject: [PATCH 12/17] Keep number of tests up to date
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0d3fa54..4d33b34 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
From 08b38039b8c7bb11b1f5bbb8f891db909cf2d389 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 27 Sep 2024 12:17:03 +0200
Subject: [PATCH 13/17] Release version 0.32.0
---
CHANGELOG.md | 5 ++++-
pytest_httpx/version.py | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f9a6043..ac8758c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+
+## [0.32.0] - 2024-09-27
### Added
- The following option is now available:
- `can_send_already_matched_responses` (boolean), defaulting to `False`.
@@ -371,7 +373,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- First release, should be considered as unstable for now as design might change.
-[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.2...HEAD
+[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.32.0...HEAD
+[0.32.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.2...v0.32.0
[0.31.2]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.1...v0.31.2
[0.31.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.0...v0.31.1
[0.31.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.30.0...v0.31.0
diff --git a/pytest_httpx/version.py b/pytest_httpx/version.py
index 4cf9fbf..b205b7c 100644
--- a/pytest_httpx/version.py
+++ b/pytest_httpx/version.py
@@ -3,4 +3,4 @@
# Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0)
# Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0)
# Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9)
-__version__ = "0.31.2"
+__version__ = "0.32.0"
From 9ddead62939c5360d31a36b089524c9affe5408d Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 27 Sep 2024 14:47:17 +0200
Subject: [PATCH 14/17] Link response reuse documentation
---
pytest_httpx/_httpx_mock.py | 12 +++++++++---
tests/test_plugin.py | 4 ++++
2 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index bf45b7a..d30175f 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -188,17 +188,23 @@ def _explain_that_no_response_was_found(
message = f"No response can be found for {RequestDescription(real_transport, request, matchers)}"
+ already_matched = []
unmatched = []
for matcher in matchers:
- if not matcher.nb_calls:
- matchers.remove(matcher)
+ if matcher.nb_calls:
+ already_matched.append(matcher)
+ else:
unmatched.append(matcher)
matchers_description = "\n".join(
- [f"- {matcher}" for matcher in unmatched + matchers]
+ [f"- {matcher}" for matcher in unmatched + already_matched]
)
if matchers_description:
message += f" amongst:\n{matchers_description}"
+ # If we could not find a response, but we have already matched responses
+ # it might be that user is expecting one of those responses to be reused
+ if already_matched and not self._options.can_send_already_matched_responses:
+ message += "\n\nIf you wanted to reuse an already matched response instead of registering it again, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-a-response-for-more-than-one-request"
return message
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index d2c2cc9..3ca420f 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -377,6 +377,8 @@ def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock):
"*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:",
"*- Already matched any request on https://foo.tld",
"*- Already matched any request on https://foo.tld",
+ "*",
+ "*If you wanted to reuse an already matched response instead of registering it again, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-a-response-for-more-than-one-request",
],
consecutive=True,
)
@@ -477,6 +479,8 @@ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx
"*- Match any request on https://foo3.tld",
"*- Already matched any request on https://foo.tld",
"*- Already matched any request on https://foo.tld",
+ "*",
+ "*If you wanted to reuse an already matched response instead of registering it again, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-a-response-for-more-than-one-request",
],
consecutive=True,
)
From 179007e7846ccfd71c707283ea2439ebf80ee69f Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 27 Sep 2024 14:49:32 +0200
Subject: [PATCH 15/17] Document the new link
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac8758c..015f449 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `can_send_already_matched_responses` (boolean), defaulting to `False`.
- Assertion failure message in case of unmatched responses is now linking documentation on how to deactivate the check.
- Assertion failure message in case of unmatched requests is now linking documentation on how to deactivate the check.
+- `httpx.TimeoutException` message issued in case of unmatched request is now linking documentation on how to reuse responses (in case some responses are already matched).
### Fixed
- Documentation now clearly state the risks associated with changing the default options.
From 50bd2572736f0a38f342389947f92d58c7815232 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 27 Sep 2024 15:04:39 +0200
Subject: [PATCH 16/17] Document how to use httpx.URL
---
README.md | 27 ++++++++++++++++++++++-----
1 file changed, 22 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 4d33b34..2bd3297 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,7 @@ Order of parameters in the query string does not matter, however order of values
```python
import httpx
+import re
from pytest_httpx import HTTPXMock
@@ -90,6 +91,20 @@ def test_url(httpx_mock: HTTPXMock):
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")
+
+
+def test_url_as_pattern(httpx_mock: HTTPXMock):
+ httpx_mock.add_response(url=re.compile(".*test.*"))
+
+ with httpx.Client() as client:
+ response = client.get("https://test_url")
+
+
+def test_url_as_httpx_url(httpx_mock: HTTPXMock):
+ httpx_mock.add_response(url=httpx.URL("https://test_url", params={"a": "1", "b": "2"}))
+
+ with httpx.Client() as client:
+ response = client.get("https://test_url?a=1&b=2")
```
#### Matching on HTTP method
@@ -163,7 +178,7 @@ def test_proxy_url(httpx_mock: HTTPXMock):
#### Matching on HTTP headers
-Use `match_headers` parameter to specify the HTTP headers to reply to.
+Use `match_headers` parameter to specify the HTTP headers (as a dict) to reply to.
Matching is performed on equality for each provided header.
@@ -181,7 +196,7 @@ def test_headers_matching(httpx_mock: HTTPXMock):
#### Matching on HTTP body
-Use `match_content` parameter to specify the full HTTP body to reply to.
+Use `match_content` parameter to specify the full HTTP body (as bytes) to reply to.
Matching is performed on equality.
@@ -292,7 +307,9 @@ def test_html_body(httpx_mock: HTTPXMock):
### Reply by streaming chunks
-Use `stream` parameter to stream chunks that you specify.
+Use `stream` parameter (as `httpx.SyncByteStream` or `httpx.AsyncByteStream`) to stream chunks that you specify.
+
+Note that `pytest_httpx.IteratorStream` can be used to provide an iterable.
```python
import httpx
@@ -349,7 +366,7 @@ content of file 1\r
### Add non 200 response
-Use `status_code` parameter to specify the HTTP status code of the response.
+Use `status_code` parameter to specify the HTTP status code (as an int) of the response.
```python
import httpx
@@ -428,7 +445,7 @@ def test_cookies(httpx_mock: HTTPXMock):
### Add HTTP/2.0 response
-Use `http_version` parameter to specify the HTTP protocol version of the response.
+Use `http_version` parameter to specify the HTTP protocol version (as a string) of the response.
```python
import httpx
From 2da18482783e36ae1eb824c685ecbab121780d63 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 27 Sep 2024 15:09:28 +0200
Subject: [PATCH 17/17] Avoid unused variables with a dedicated name
---
tests/test_httpx_async.py | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index c304ca7..5d2d247 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -185,7 +185,7 @@ async def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None:
]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
- async for part in response.aiter_raw():
+ async for _ in response.aiter_raw():
pass # pragma: no cover
async with client.stream(method="GET", url="https://test_url") as response:
@@ -195,7 +195,7 @@ async def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None:
]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
- async for part in response.aiter_raw():
+ async for _ in response.aiter_raw():
pass # pragma: no cover
@@ -214,7 +214,7 @@ async def test_content_response_streaming(httpx_mock: HTTPXMock) -> None:
]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
- async for part in response.aiter_raw():
+ async for _ in response.aiter_raw():
pass # pragma: no cover
async with client.stream(method="GET", url="https://test_url") as response:
@@ -223,7 +223,7 @@ async def test_content_response_streaming(httpx_mock: HTTPXMock) -> None:
]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
- async for part in response.aiter_raw():
+ async for _ in response.aiter_raw():
pass # pragma: no cover
@@ -242,7 +242,7 @@ async def test_text_response_streaming(httpx_mock: HTTPXMock) -> None:
]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
- async for part in response.aiter_raw():
+ async for _ in response.aiter_raw():
pass # pragma: no cover
async with client.stream(method="GET", url="https://test_url") as response:
@@ -251,7 +251,7 @@ async def test_text_response_streaming(httpx_mock: HTTPXMock) -> None:
]
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
- async for part in response.aiter_raw():
+ async for _ in response.aiter_raw():
pass # pragma: no cover
@@ -265,14 +265,14 @@ async def test_default_response_streaming(httpx_mock: HTTPXMock) -> None:
assert [part async for part in response.aiter_raw()] == []
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
- async for part in response.aiter_raw():
+ async for _ in response.aiter_raw():
pass # pragma: no cover
async with client.stream(method="GET", url="https://test_url") as response:
assert [part async for part in response.aiter_raw()] == []
# Assert that stream still behaves the proper way (can only be consumed once per request)
with pytest.raises(httpx.StreamConsumed):
- async for part in response.aiter_raw():
+ async for _ in response.aiter_raw():
pass # pragma: no cover