Skip to content

Commit

Permalink
Merge pull request #157 from Colin-b/develop
Browse files Browse the repository at this point in the history
Release 0.31.2
  • Loading branch information
Colin-b authored Sep 23, 2024
2 parents 75c308e + 97da972 commit 0cbba7c
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 71 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.31.2] - 2024-09-23
### Fixed
- `httpx_mock` marker can now be defined at different levels for a single test.

## [0.31.1] - 2024-09-22
### Fixed
- It is now possible to match on content provided as async iterable by the client.
Expand Down Expand Up @@ -341,7 +345,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.1...HEAD
[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.2...HEAD
[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
[0.30.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.29.0...v0.30.0
Expand Down
135 changes: 78 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Build status" src="https://github.com/Colin-b/pytest_httpx/workflows/Release/badge.svg"></a>
<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a>
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-215 passed-blue"></a>
<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-216 passed-blue"></a>
<a href="https://pypi.org/project/pytest-httpx/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/pytest_httpx"></a>
</p>

> Version 1.0.0 will be released once httpx is considered as stable (release of 1.0.0).
>
> However, current state can be considered as stable.
Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixture will make sure every [`httpx`](https://www.python-httpx.org) request will be replied to with user provided responses.
Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixture will make sure every [`httpx`](https://www.python-httpx.org) request will be replied to with user provided responses ([unless some hosts are explicitly skipped](#do-not-mock-some-requests)).

- [Add responses](#add-responses)
- [JSON body](#add-json-response)
Expand All @@ -25,7 +25,10 @@ Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixt
- [Add dynamic responses](#dynamic-responses)
- [Raising exceptions](#raising-exceptions)
- [Check requests](#check-sent-requests)
- [Do not mock some requests](#do-not-mock-some-requests)
- [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)
- [Do not mock some requests](#do-not-mock-some-requests)
- [Migrating](#migrating-to-pytest-httpx)
- [responses](#from-responses)
- [aioresponses](#from-aioresponses)
Expand Down Expand Up @@ -54,21 +57,7 @@ async def test_something_async(httpx_mock):
response = await client.get("https://test_url")
```

If all registered responses are not sent back during test execution, the test case will fail at teardown.

This behavior can be disabled thanks to the `httpx_mock` marker:

```python
import pytest

# For whole module
pytestmark = pytest.mark.httpx_mock(assert_all_responses_were_requested=False)

# For specific test
@pytest.mark.httpx_mock(assert_all_responses_were_requested=True)
def test_something(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.

Expand Down Expand Up @@ -458,21 +447,7 @@ You can perform custom manipulation upon request reception by registering callba

Callback should expect one parameter, the received [`httpx.Request`](https://www.python-httpx.org/api/#request).

If all callbacks are not executed during test execution, the test case will fail at teardown.

This behavior can be disabled thanks to the `httpx_mock` marker:

```python
import pytest

# For whole module
pytestmark = pytest.mark.httpx_mock(assert_all_responses_were_requested=False)

# For specific test
@pytest.mark.httpx_mock(assert_all_responses_were_requested=True)
def test_something(httpx_mock):
...
```
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).

Expand Down Expand Up @@ -571,21 +546,7 @@ def test_timeout(httpx_mock: HTTPXMock):
## Check sent requests

The best way to ensure the content of your requests is still to use the `match_headers` and / or `match_content` parameters when adding a response.
In the same spirit, ensuring that no request was issued does not necessarily require any code.

Note that default behavior is to assert that all requests were expected. You can turn this off (at your own risk of not spotting regression in your code base) using the `httpx_mock` marker:

```python
import pytest

# For whole module
pytestmark = pytest.mark.httpx_mock(assert_all_requests_were_expected=False)

# For specific test
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_something(httpx_mock):
...
```
In the same spirit, ensuring that no request was issued does not necessarily require any code [(unless you turned `assert_all_requests_were_expected` option off)](#allow-to-not-register-responses-for-every-request).

In any case, you always have the ability to retrieve the requests that were issued.

Expand Down Expand Up @@ -667,26 +628,86 @@ Matching is performed on equality. You can however use `unittest.mock.ANY` to do

Note that `match_content` cannot be provided if `match_json` is also provided.

## Do not mock some requests
## Configuring httpx_mock

By default, `pytest-httpx` will mock every request.
The `httpx_mock` marker is available and can be used to change the default behavior of the `httpx_mock` fixture.

But, for instance, in case you want to write integration tests with other servers, you might want to let some requests go through.
Refer to [available options](#available-options) for an exhaustive list of options that can be set [per test](#per-test), [per module](#per-module) or even [on the whole test suite](#for-the-whole-test-suite).

To do so, you can use the `httpx_mock` marker:
### Per test

```python
import pytest

# For whole module
pytestmark = pytest.mark.httpx_mock(non_mocked_hosts=["my_local_test_host", "my_other_test_host"])

# For specific test
@pytest.mark.httpx_mock(non_mocked_hosts=["my_local_test_host"])
@pytest.mark.httpx_mock(assert_all_responses_were_requested=True)
def test_something(httpx_mock):
...
```

### Per module

```python
import pytest

pytestmark = pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
```

### For the whole test suite

This should be set in the root `conftest.py` file.
```python
import pytest

def pytest_collection_modifyitems(session, config, items):
for item in items:
item.add_marker(pytest.mark.httpx_mock(assert_all_responses_were_requested=False))
```

### Available options

#### Allow to register more responses than what will be requested

By default, `pytest-httpx` will ensure that every response was requested during test execution.

You can use the `httpx_mock` marker `assert_all_responses_were_requested` option to allow fewer requests than what you registered responses for.

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)
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()
```

#### Allow to not register responses for every request

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!

```python
import pytest
import httpx

@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_more_requests_than_expected(httpx_mock):
with httpx.Client() as client:
# Even if this request was not expected, the test will not fail at teardown
with pytest.raises(httpx.TimeoutException):
client.get("https://test_url")
```

#### Do not mock some requests

By default, `pytest-httpx` will mock every request.

But, for instance, in case you want to write integration tests with other servers, you might want to let some requests go through.

To do so, you can use the `httpx_mock` marker `non_mocked_hosts` option and provide a list of non mocked hosts.
Every other requested hosts will be mocked as in the following example

```python
Expand Down
8 changes: 6 additions & 2 deletions pytest_httpx/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections.abc import Generator
from operator import methodcaller

import httpx
import pytest
Expand All @@ -20,8 +21,11 @@ def httpx_mock(
monkeypatch: MonkeyPatch,
request: FixtureRequest,
) -> Generator[HTTPXMock, None, None]:
marker = request.node.get_closest_marker("httpx_mock")
options = HTTPXMockOptions.from_marker(marker) if marker else HTTPXMockOptions()
options = {}
for marker in request.node.iter_markers("httpx_mock"):
options = marker.kwargs | options
__tracebackhide__ = methodcaller("errisinstance", TypeError)
options = HTTPXMockOptions(**options)

mock = HTTPXMock()

Expand Down
12 changes: 2 additions & 10 deletions pytest_httpx/_httpx_mock.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import copy
import inspect
from operator import methodcaller
from typing import Union, Optional, Callable, Any, NoReturn, AsyncIterable
from collections.abc import Awaitable, Iterable
from typing import Union, Optional, Callable, Any, NoReturn
from collections.abc import Awaitable

import httpx
from pytest import Mark

from pytest_httpx import _httpx_internals
from pytest_httpx._pretty_print import RequestDescription
Expand All @@ -32,12 +30,6 @@ def __init__(
]
self.non_mocked_hosts = [*non_mocked_hosts, *missing_www]

@classmethod
def from_marker(cls, marker: Mark) -> "HTTPXMockOptions":
"""Initialise from a marker so that the marker kwargs raise an error if incorrect."""
__tracebackhide__ = methodcaller("errisinstance", TypeError)
return cls(**marker.kwargs)


class HTTPXMock:
def __init__(self) -> None:
Expand Down
2 changes: 1 addition & 1 deletion pytest_httpx/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.1"
__version__ = "0.31.2"
55 changes: 55 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,61 @@ async def test_httpx_mock_non_mocked_hosts_async(httpx_mock):
result.assert_outcomes(passed=1)


def test_httpx_mock_options_on_multi_levels_are_aggregated(testdir: Testdir) -> None:
"""
Test case ensures that every level provides one parameter that should be used in the end
global (actually registered AFTER module): assert_all_responses_were_requested (tested by putting unused response)
module: assert_all_requests_were_expected (tested by not mocking one URL)
test: non_mocked_hosts (tested by calling 3 URls, 2 mocked, the other one not)
"""
testdir.makeconftest(
"""
import pytest
def pytest_collection_modifyitems(session, config, items):
for item in items:
item.add_marker(pytest.mark.httpx_mock(assert_all_responses_were_requested=False))
"""
)
testdir.makepyfile(
"""
import httpx
import pytest
pytestmark = pytest.mark.httpx_mock(assert_all_requests_were_expected=False, non_mocked_hosts=["https://foo.tld"])
@pytest.mark.asyncio
@pytest.mark.httpx_mock(non_mocked_hosts=["localhost"])
async def test_httpx_mock_non_mocked_hosts_async(httpx_mock):
httpx_mock.add_response(url="https://foo.tld", headers={"x-pytest-httpx": "this was mocked"})
# This response will never be used, testing that assert_all_responses_were_requested is handled
httpx_mock.add_response(url="https://never_called.url")
async with httpx.AsyncClient() as client:
# Assert that previously set non_mocked_hosts was overridden
response = await client.get("https://foo.tld")
assert response.headers["x-pytest-httpx"] == "this was mocked"
# Assert that latest non_mocked_hosts is handled
with pytest.raises(httpx.ConnectError):
await client.get("https://localhost:5005")
# Assert that assert_all_requests_were_expected is the one at module level
with pytest.raises(httpx.TimeoutException):
await client.get("https://unexpected.url")
# Assert that 2 requests out of 3 were mocked
assert len(httpx_mock.get_requests()) == 2
"""
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)


def test_invalid_marker(testdir: Testdir) -> None:
"""
Unknown marker keyword arguments should raise a TypeError.
Expand Down

0 comments on commit 0cbba7c

Please sign in to comment.