Skip to content

Commit

Permalink
Allow multi match per response
Browse files Browse the repository at this point in the history
  • Loading branch information
Colin-b committed Nov 18, 2024
1 parent 066b4d2 commit ceb09f0
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- `assert_requested` parameter is now available on responses and callbacks registration. Allowing to add optional responses while keeping other responses as mandatory. Refer to documentation for more details.
- `can_send_already_matched` parameter is now available on responses and callbacks registration. Allowing to add multi-match responses while keeping other responses as single-match. Refer to documentation for more details.

## [0.33.0] - 2024-10-28
### Added
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,22 @@ def test_more_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 `can_send_already_matched_responses` option to allow multiple requests to match the same registered response.
If you want to add a response once, while allowing it to match more than once, you can use the `can_send_already_matched` parameter when [registering a response](#add-responses) or [a callback](#add-callbacks).

```python
import httpx

def test_more_requests_than_responses(httpx_mock):
httpx_mock.add_response(can_send_already_matched=True)
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")
```

If you don't have control over the response registration process (shared fixtures),
and you want to allow multiple requests to match the same registered response,
you can use the `httpx_mock` marker `can_send_already_matched_responses` option.

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.

Expand Down
4 changes: 2 additions & 2 deletions pytest_httpx/_httpx_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def _explain_that_no_response_was_found(
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:
if any(not matcher.can_send_already_matched for matcher in already_matched):
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
Expand Down Expand Up @@ -245,7 +245,7 @@ def _get_callback(
return callback

# Or the last registered (if it can be reused)
if self._options.can_send_already_matched_responses:
if matcher.can_send_already_matched:
matcher.nb_calls += 1
return callback

Expand Down
4 changes: 3 additions & 1 deletion pytest_httpx/_request_matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def __init__(
match_files: Optional[Any] = None,
match_extensions: Optional[dict[str, Any]] = None,
assert_requested: Optional[bool] = None,
can_send_already_matched: Optional[bool] = None,
):
self._options = options
self.nb_calls = 0
Expand All @@ -57,6 +58,7 @@ def __init__(
)
self.extensions = match_extensions
self.assert_requested = options.assert_all_responses_were_requested if assert_requested is None else assert_requested
self.can_send_already_matched = options.can_send_already_matched_responses if can_send_already_matched is None else can_send_already_matched
if self._is_matching_body_more_than_one_way():
raise ValueError(
"Only one way of matching against the body can be provided. "
Expand Down Expand Up @@ -184,7 +186,7 @@ def should_have_matched(self) -> bool:
return self.assert_requested and not self.nb_calls

def __str__(self) -> str:
if self._options.can_send_already_matched_responses:
if self.can_send_already_matched:
matcher_description = f"Match {self.method or 'every'} request"
else:
matcher_description = "Already matched" if self.nb_calls else "Match"
Expand Down
20 changes: 20 additions & 0 deletions tests/test_httpx_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -2472,3 +2472,23 @@ async def test_mandatory_response_matched(httpx_mock: HTTPXMock) -> None:
async with httpx.AsyncClient() as client:
response = await client.get("https://test_url2")
assert response.content == b""


@pytest.mark.asyncio
async def test_multi_response_matched_once(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", can_send_already_matched=True)

async with httpx.AsyncClient() as client:
response = await client.get("https://test_url")
assert response.content == b""


@pytest.mark.asyncio
async def test_multi_response_matched_twice(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", can_send_already_matched=True)

async with httpx.AsyncClient() as client:
response1 = await client.get("https://test_url")
response2 = await client.get("https://test_url")
assert response1.content == b""
assert response2.content == b""
18 changes: 18 additions & 0 deletions tests/test_httpx_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -2163,3 +2163,21 @@ def test_mandatory_response_matched(httpx_mock: HTTPXMock) -> None:
with httpx.Client() as client:
response = client.get("https://test_url2")
assert response.content == b""


def test_multi_response_matched_once(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", can_send_already_matched=True)

with httpx.Client() as client:
response = client.get("https://test_url")
assert response.content == b""


def test_multi_response_matched_twice(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url="https://test_url", can_send_already_matched=True)

with httpx.Client() as client:
response1 = client.get("https://test_url")
response2 = client.get("https://test_url")
assert response1.content == b""
assert response2.content == b""
24 changes: 24 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,3 +717,27 @@ def test_mandatory_response_not_matched(httpx_mock):
],
consecutive=True,
)


def test_multi_response_not_matched(testdir: Testdir) -> None:
testdir.makepyfile(
"""
import httpx
def test_multi_response_not_matched(httpx_mock):
httpx_mock.add_response(url="https://test_url2", can_send_already_matched=True)
"""
)
result = testdir.runpytest()
result.assert_outcomes(errors=1, passed=1)
# Assert the teardown assertion failure
result.stdout.fnmatch_lines(
[
"*AssertionError: The following responses are mocked but not requested:",
"* - Match every request on https://test_url2",
"* ",
"* 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,
)

0 comments on commit ceb09f0

Please sign in to comment.