Skip to content

Commit

Permalink
Already matched responses are not sent by default anymore
Browse files Browse the repository at this point in the history
  • Loading branch information
Colin-b committed Sep 26, 2024
1 parent e3922ab commit f663eee
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 56 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
93 changes: 44 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<a href="https://pypi.org/project/pytest-httpx/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/pytest_httpx"></a>
</p>

> [!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.
Expand All @@ -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)
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion pytest_httpx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
)
12 changes: 9 additions & 3 deletions pytest_httpx/_httpx_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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]:
"""
Expand Down
Loading

0 comments on commit f663eee

Please sign in to comment.