diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6d99a8b..f0caf98 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,8 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] - pytest-major-version: ['7', '8'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13.0-rc.2'] steps: - uses: actions/checkout@v4 @@ -21,7 +20,6 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -e .[testing] - python -m pip install pytest~=${{ matrix.pytest-major-version }}.0 - name: Test run: | pytest --cov=pytest_httpx --cov-fail-under=100 --cov-report=term-missing --runpytest=subprocess diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43c0278..8856f1b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: - repo: https://github.com/psf/black - rev: 24.1.1 + rev: 24.8.0 hooks: - id: black \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d7855f..841a254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [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. +- 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 + pytestmark = pytest.mark.httpx_mock(assert_all_responses_were_requested=False) + + # Or to specific tests + @pytest.mark.httpx_mock(non_mocked_hosts=[...]) + def test_foo(httpx_mock): + ... + ``` + - The following options are available: + - `assert_all_responses_were_requested` (boolean), defaulting to `True`. + - `assert_all_requests_were_expected` (boolean), defaulting to `True`. + - `non_mocked_hosts` (iterable), defaulting to an empty list, meaning all hosts are mocked. +- `httpx_mock.reset` do not expect any parameter anymore and will only reset the mock state (no assertions will be performed). + +### Removed +- `pytest` `7` is not supported anymore (`pytest` `8` has been out for 9 months already). +- `assert_all_responses_were_requested` fixture is not available anymore, use `pytest.mark.httpx_mock(assert_all_responses_were_requested=False)` instead. +- `non_mocked_hosts` fixture is not available anymore, use `pytest.mark.httpx_mock(non_mocked_hosts=[])` instead. + ## [0.30.0] - 2024-02-21 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.27.\* @@ -312,7 +337,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.30.0...HEAD +[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.0...HEAD +[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 [0.29.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.28.0...v0.29.0 [0.28.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.27.0...v0.28.0 diff --git a/README.md b/README.md index f463cf2..3095b3b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Build status Coverage Code style: black -Number of tests +Number of tests Number of downloads

@@ -56,14 +56,18 @@ 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. -This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture: +This behavior can be disabled thanks to the `httpx_mock` marker: ```python import pytest -@pytest.fixture -def assert_all_responses_were_requested() -> bool: - return False +# 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): + ... ``` Default response is a HTTP/1.1 200 (OK) without any body. @@ -456,14 +460,18 @@ 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. -This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture: +This behavior can be disabled thanks to the `httpx_mock` marker: ```python import pytest -@pytest.fixture -def assert_all_responses_were_requested() -> bool: - return False +# 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): + ... ``` Note that callbacks are considered as responses, and thus are [selected the same way](#how-response-is-selected). @@ -552,6 +560,7 @@ import pytest from pytest_httpx import HTTPXMock +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_timeout(httpx_mock: HTTPXMock): with httpx.Client() as client: with pytest.raises(httpx.TimeoutException): @@ -564,6 +573,20 @@ def test_timeout(httpx_mock: HTTPXMock): 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 any case, you always have the ability to retrieve the requests that were issued. As in the following samples: @@ -650,14 +673,18 @@ By default, `pytest-httpx` will mock every request. But, for instance, in case you want to write integration tests with other servers, you might want to let some requests go through. -To do so, you can use the `non_mocked_hosts` fixture: +To do so, you can use the `httpx_mock` marker: ```python import pytest -@pytest.fixture -def non_mocked_hosts() -> list: - return ["my_local_test_host", "my_other_test_host"] +# 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"]) +def test_something(httpx_mock): + ... ``` Every other requested hosts will be mocked as in the following example @@ -666,11 +693,7 @@ Every other requested hosts will be mocked as in the following example import pytest import httpx -@pytest.fixture -def non_mocked_hosts() -> list: - return ["my_local_test_host"] - - +@pytest.mark.httpx_mock(non_mocked_hosts=["my_local_test_host"]) def test_partial_mock(httpx_mock): httpx_mock.add_response() diff --git a/pyproject.toml b/pyproject.toml index 841b354..23bfe63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ maintainers = [ ] keywords = [ "httpx", - "mock", "pytest", "testing", ] @@ -38,7 +37,7 @@ classifiers = [ ] dependencies = [ "httpx==0.27.*", - "pytest>=7,<9", + "pytest==8.*", ] dynamic = ["version"] @@ -51,16 +50,17 @@ issues = "https://github.com/Colin-b/pytest_httpx/issues" [project.optional-dependencies] testing = [ # Used to check coverage - "pytest-cov==4.*", + "pytest-cov==5.*", # Used to run async tests - "pytest-asyncio==0.23.*", + "pytest-asyncio==0.24.*", ] [project.entry-points.pytest11] pytest_httpx = "pytest_httpx" -[tool.setuptools.packages.find] -exclude = ["tests*"] - [tool.setuptools.dynamic] version = {attr = "pytest_httpx.version.__version__"} + +[tool.pytest.ini_options] +# Silence deprecation warnings about option "asyncio_default_fixture_loop_scope" +asyncio_default_fixture_loop_scope = "function" diff --git a/pytest_httpx/__init__.py b/pytest_httpx/__init__.py index 3fe8071..28f400d 100644 --- a/pytest_httpx/__init__.py +++ b/pytest_httpx/__init__.py @@ -1,11 +1,10 @@ from collections.abc import Generator -from typing import List import httpx import pytest -from pytest import MonkeyPatch +from pytest import Config, FixtureRequest, MonkeyPatch -from pytest_httpx._httpx_mock import HTTPXMock +from pytest_httpx._httpx_mock import HTTPXMock, HTTPXMockOptions from pytest_httpx._httpx_internals import IteratorStream from pytest_httpx.version import __version__ @@ -16,27 +15,13 @@ ) -@pytest.fixture -def assert_all_responses_were_requested() -> bool: - return True - - -@pytest.fixture -def non_mocked_hosts() -> List[str]: - return [] - - @pytest.fixture def httpx_mock( monkeypatch: MonkeyPatch, - assert_all_responses_were_requested: bool, - non_mocked_hosts: List[str], + request: FixtureRequest, ) -> Generator[HTTPXMock, None, None]: - # Ensure redirections to www hosts are handled transparently. - missing_www = [ - f"www.{host}" for host in non_mocked_hosts if not host.startswith("www.") - ] - non_mocked_hosts += missing_www + marker = request.node.get_closest_marker("httpx_mock") + options = HTTPXMockOptions.from_marker(marker) if marker else HTTPXMockOptions() mock = HTTPXMock() @@ -46,7 +31,7 @@ def httpx_mock( def mocked_handle_request( transport: httpx.HTTPTransport, request: httpx.Request ) -> httpx.Response: - if request.url.host in non_mocked_hosts: + if request.url.host in options.non_mocked_hosts: return real_handle_request(transport, request) return mock._handle_request(transport, request) @@ -62,7 +47,7 @@ def mocked_handle_request( async def mocked_handle_async_request( transport: httpx.AsyncHTTPTransport, request: httpx.Request ) -> httpx.Response: - if request.url.host in non_mocked_hosts: + if request.url.host in options.non_mocked_hosts: return await real_handle_async_request(transport, request) return await mock._handle_async_request(transport, request) @@ -73,4 +58,14 @@ async def mocked_handle_async_request( ) yield mock - mock.reset(assert_all_responses_were_requested) + try: + mock._assert_options(options) + finally: + mock.reset() + + +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.", + ) diff --git a/pytest_httpx/_httpx_internals.py b/pytest_httpx/_httpx_internals.py index da21797..66199ff 100644 --- a/pytest_httpx/_httpx_internals.py +++ b/pytest_httpx/_httpx_internals.py @@ -2,13 +2,10 @@ from typing import ( Union, Dict, - Sequence, Tuple, - Iterable, - AsyncIterator, - Iterator, Optional, ) +from collections.abc import Sequence, Iterable, AsyncIterator, Iterator import httpcore import httpx @@ -19,10 +16,10 @@ # Those types are internally defined within httpx._types HeaderTypes = Union[ httpx.Headers, - Dict[str, str], - Dict[bytes, bytes], - Sequence[Tuple[str, str]], - Sequence[Tuple[bytes, bytes]], + dict[str, str], + dict[bytes, bytes], + Sequence[tuple[str, str]], + Sequence[tuple[bytes, bytes]], ] @@ -30,8 +27,7 @@ class IteratorStream(AsyncIteratorByteStream, IteratorByteStream): def __init__(self, stream: Iterable[bytes]): class Stream: def __iter__(self) -> Iterator[bytes]: - for chunk in stream: - yield chunk + yield from stream async def __aiter__(self) -> AsyncIterator[bytes]: for chunk in stream: diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 941b050..33ac08b 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -1,14 +1,44 @@ import copy import inspect -from typing import Union, Optional, Callable, Any, Awaitable +from operator import methodcaller +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 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, + 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 + + 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] + + @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: self._requests: list[ @@ -25,6 +55,7 @@ def __init__(self) -> None: ], ] ] = [] + self._requests_not_matched: list[httpx.Request] = [] def add_response( self, @@ -135,10 +166,7 @@ def _handle_request( if response: return _unread(response) - raise httpx.TimeoutException( - self._explain_that_no_response_was_found(real_transport, request), - request=request, - ) + self._request_not_matched(real_transport, request) async def _handle_async_request( self, @@ -156,6 +184,14 @@ async def _handle_async_request( response = await response return _unread(response) + self._request_not_matched(real_transport, request) + + def _request_not_matched( + self, + real_transport: Union[httpx.AsyncHTTPTransport, httpx.HTTPTransport], + request: httpx.Request, + ) -> NoReturn: + self._requests_not_matched.append(request) raise httpx.TimeoutException( self._explain_that_no_response_was_found(real_transport, request), request=request, @@ -247,23 +283,28 @@ def get_request(self, **matchers: Any) -> Optional[httpx.Request]: ), f"More than one request ({len(requests)}) matched, use get_requests instead." return requests[0] if requests else None - def reset(self, assert_all_responses_were_requested: bool) -> None: + def reset(self) -> None: self._requests.clear() - not_called = self._reset_callbacks() + self._callbacks.clear() + self._requests_not_matched.clear() - if assert_all_responses_were_requested: - matchers_description = "\n".join([str(matcher) for matcher in not_called]) + def _assert_options(self, options: HTTPXMockOptions) -> None: + if options.assert_all_responses_were_requested: + callbacks_not_executed = [ + matcher for matcher, _ in self._callbacks if not matcher.nb_calls + ] + matchers_description = "\n".join( + [str(matcher) for matcher in callbacks_not_executed] + ) assert ( - not not_called + not callbacks_not_executed ), f"The following responses are mocked but not requested:\n{matchers_description}" - def _reset_callbacks(self) -> list[_RequestMatcher]: - callbacks_not_executed = [ - matcher for matcher, _ in self._callbacks if not matcher.nb_calls - ] - self._callbacks.clear() - return callbacks_not_executed + 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}" def _unread(response: httpx.Response) -> httpx.Response: diff --git a/pytest_httpx/_pretty_print.py b/pytest_httpx/_pretty_print.py index 2d6f336..f79fd1e 100644 --- a/pytest_httpx/_pretty_print.py +++ b/pytest_httpx/_pretty_print.py @@ -17,15 +17,13 @@ def __init__( self.request = request headers_encoding = request.headers.encoding - self.expected_headers = set( - [ - # httpx uses lower cased header names as internal key - header.lower().encode(headers_encoding) - for matcher in matchers - if matcher.headers - for header in matcher.headers - ] - ) + self.expected_headers = { + # httpx uses lower cased header names as internal key + header.lower().encode(headers_encoding) + for matcher in matchers + if matcher.headers + for header in matcher.headers + } self.expect_body = any( [ matcher.content is not None or matcher.json is not None diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py index 1bb590c..04c4ab4 100644 --- a/pytest_httpx/_request_matcher.py +++ b/pytest_httpx/_request_matcher.py @@ -1,6 +1,7 @@ import json import re -from typing import Optional, Union, Pattern, Any +from typing import Optional, Union, Any +from re import Pattern import httpx diff --git a/pytest_httpx/version.py b/pytest_httpx/version.py index 5b58ad4..63fd052 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.30.0" +__version__ = "0.31.0" diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py index 89607ff..1d5d721 100644 --- a/tests/test_httpx_async.py +++ b/tests/test_httpx_async.py @@ -13,6 +13,7 @@ @pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_without_response(httpx_mock: HTTPXMock) -> None: with pytest.raises(Exception) as exception_info: async with httpx.AsyncClient() as client: @@ -61,6 +62,9 @@ async def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_url_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url") @@ -73,11 +77,11 @@ async def test_url_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests on https://test_url""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=2") @@ -91,9 +95,6 @@ async def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests on https://test_url?a=1&a=2""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio async def test_method_matching(httpx_mock: HTTPXMock) -> None: @@ -108,6 +109,9 @@ async def test_method_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_method_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get") @@ -120,9 +124,6 @@ async def test_method_not_matching(httpx_mock: HTTPXMock) -> None: Match GET requests""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio async def test_with_one_response(httpx_mock: HTTPXMock) -> None: @@ -1107,6 +1108,9 @@ async def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_multi_value_headers_not_matching_single_value_issued( httpx_mock: HTTPXMock, ) -> None: @@ -1127,11 +1131,11 @@ async def test_multi_value_headers_not_matching_single_value_issued( Match all requests with {'my-custom-header': 'value1'} headers""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_multi_value_headers_not_matching_multi_value_issued( httpx_mock: HTTPXMock, ) -> None: @@ -1152,11 +1156,11 @@ async def test_multi_value_headers_not_matching_multi_value_issued( Match all requests with {'my-custom-header': 'value1, value2'} headers""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={"user-agent": f"python-httpx/{httpx.__version__}"} @@ -1171,11 +1175,11 @@ async def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None: Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1194,11 +1198,11 @@ async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_url_not_matching_upper_case_headers_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1216,9 +1220,6 @@ async def test_url_not_matching_upper_case_headers_matching( Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio async def test_content_matching(httpx_mock: HTTPXMock) -> None: @@ -1239,6 +1240,9 @@ async def test_proxy_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy") @@ -1251,11 +1255,11 @@ async def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests with http://my_test_proxy proxy URL""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy") @@ -1268,9 +1272,6 @@ async def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: Match all requests with http://my_test_proxy proxy URL""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio async def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None: @@ -1337,6 +1338,9 @@ async def test_request_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_content=b"This is the body") @@ -1349,9 +1353,6 @@ async def test_content_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests with b'This is the body' body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio async def test_json_matching(httpx_mock: HTTPXMock) -> None: @@ -1372,6 +1373,9 @@ async def test_json_partial_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) @@ -1384,11 +1388,11 @@ async def test_json_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests with {'a': 1, 'b': 2} json body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_json={"a": 1, "b": 2}, @@ -1404,11 +1408,11 @@ async def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) @@ -1421,9 +1425,6 @@ async def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: Match all requests with {'a': 1, 'b': 2} json body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio async def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: @@ -1438,6 +1439,9 @@ async def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1456,11 +1460,11 @@ async def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1479,11 +1483,11 @@ async def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1502,9 +1506,6 @@ async def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio async def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: @@ -1520,6 +1521,9 @@ async def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> No @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_headers_not_matching_and_url_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1541,11 +1545,11 @@ async def test_headers_not_matching_and_url_and_content_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_url_and_headers_not_matching_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1567,11 +1571,11 @@ async def test_url_and_headers_not_matching_and_content_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1593,11 +1597,11 @@ async def test_url_and_headers_matching_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1619,11 +1623,11 @@ async def test_headers_matching_and_url_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1645,11 +1649,11 @@ async def test_url_matching_and_headers_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url2", @@ -1669,9 +1673,6 @@ async def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) - 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio async def test_method_and_url_and_headers_and_content_matching( @@ -1690,6 +1691,9 @@ async def test_method_and_url_and_headers_and_content_matching( @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_headers_not_matching_and_method_and_url_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1712,11 +1716,11 @@ async def test_headers_not_matching_and_method_and_url_and_content_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_url_and_headers_not_matching_and_method_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1739,11 +1743,11 @@ async def test_url_and_headers_not_matching_and_method_and_content_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_method_and_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1766,11 +1770,11 @@ async def test_method_and_url_and_headers_matching_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_method_and_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1793,11 +1797,11 @@ async def test_method_and_headers_matching_and_url_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_method_and_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1820,11 +1824,11 @@ async def test_method_and_url_matching_and_headers_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_method_matching_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1847,11 +1851,11 @@ async def test_method_matching_and_url_and_headers_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) async def test_method_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1874,9 +1878,6 @@ async def test_method_and_url_and_headers_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - @pytest.mark.asyncio async def test_header_as_str_tuple_list(httpx_mock: HTTPXMock) -> None: @@ -1980,7 +1981,7 @@ async def test_reset_is_removing_requests(httpx_mock: HTTPXMock) -> None: assert len(httpx_mock.get_requests()) == 1 - httpx_mock.reset(assert_all_responses_were_requested=False) + httpx_mock.reset() assert len(httpx_mock.get_requests()) == 0 diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py index e92e2be..3cbfb3f 100644 --- a/tests/test_httpx_sync.py +++ b/tests/test_httpx_sync.py @@ -9,6 +9,7 @@ from pytest_httpx import HTTPXMock +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_without_response(httpx_mock: HTTPXMock) -> None: with pytest.raises(Exception) as exception_info: with httpx.Client() as client: @@ -53,6 +54,9 @@ def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_url_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url") @@ -65,10 +69,10 @@ def test_url_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests on https://test_url""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=2") @@ -82,9 +86,6 @@ def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests on https://test_url?a=1&a=2""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - def test_method_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get") @@ -97,6 +98,9 @@ def test_method_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_method_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get") @@ -109,9 +113,6 @@ def test_method_not_matching(httpx_mock: HTTPXMock) -> None: Match GET requests""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - def test_with_one_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", content=b"test content") @@ -140,6 +141,9 @@ def test_response_with_html_string_body(httpx_mock: HTTPXMock) -> None: assert response.text == "test content" +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( method="GET", @@ -155,9 +159,6 @@ def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) -> Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( @@ -880,6 +881,9 @@ def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_multi_value_headers_not_matching_single_value_issued( httpx_mock: HTTPXMock, ) -> None: @@ -900,10 +904,10 @@ def test_multi_value_headers_not_matching_single_value_issued( Match all requests with {'my-custom-header': 'value1'} headers""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_multi_value_headers_not_matching_multi_value_issued( httpx_mock: HTTPXMock, ) -> None: @@ -924,10 +928,10 @@ def test_multi_value_headers_not_matching_multi_value_issued( Match all requests with {'my-custom-header': 'value1, value2'} headers""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={"user-agent": f"python-httpx/{httpx.__version__}"} @@ -942,10 +946,10 @@ def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None: Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_headers_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -964,9 +968,6 @@ def test_headers_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - def test_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_content=b"This is the body") @@ -984,6 +985,9 @@ def test_proxy_matching(httpx_mock: HTTPXMock) -> None: assert response.read() == b"" +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy") @@ -996,10 +1000,10 @@ def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests with http://my_test_proxy proxy URL""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy") @@ -1012,9 +1016,6 @@ def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: Match all requests with http://my_test_proxy proxy URL""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response() @@ -1072,6 +1073,9 @@ def test_request_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: assert httpx_mock.get_request(proxy_url="http://my_test_proxy/") +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_content=b"This is the body") @@ -1084,9 +1088,6 @@ def test_content_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests with b'This is the body' body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - def test_match_json_and_match_content_error(httpx_mock: HTTPXMock) -> None: with pytest.raises(ValueError) as exception_info: @@ -1114,6 +1115,9 @@ def test_json_partial_matching(httpx_mock: HTTPXMock) -> None: assert response.read() == b"" +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) @@ -1126,10 +1130,10 @@ def test_json_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests with {'a': 1, 'b': 2} json body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_json={"a": 1, "b": 2}, @@ -1145,10 +1149,10 @@ def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) @@ -1161,9 +1165,6 @@ def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: Match all requests with {'a': 1, 'b': 2} json body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( @@ -1176,6 +1177,9 @@ def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1194,10 +1198,10 @@ def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> Non Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1216,10 +1220,10 @@ def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> Non Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1238,9 +1242,6 @@ def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( @@ -1254,6 +1255,9 @@ def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_headers_not_matching_and_url_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1275,10 +1279,10 @@ def test_headers_not_matching_and_url_and_content_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_url_and_headers_not_matching_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1300,10 +1304,10 @@ def test_url_and_headers_not_matching_and_content_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1325,10 +1329,10 @@ def test_url_and_headers_matching_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1350,10 +1354,10 @@ def test_headers_matching_and_url_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1375,10 +1379,10 @@ def test_url_matching_and_headers_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url2", @@ -1398,9 +1402,6 @@ def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - def test_method_and_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( @@ -1415,6 +1416,9 @@ def test_method_and_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) assert response.content == b"" +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_headers_not_matching_and_method_and_url_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1437,10 +1441,10 @@ def test_headers_not_matching_and_method_and_url_and_content_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_url_and_headers_not_matching_and_method_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1463,10 +1467,10 @@ def test_url_and_headers_not_matching_and_method_and_content_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_method_and_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1489,10 +1493,10 @@ def test_method_and_url_and_headers_matching_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_method_and_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1515,10 +1519,10 @@ def test_method_and_headers_matching_and_url_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_method_and_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1541,10 +1545,10 @@ def test_method_and_url_matching_and_headers_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_method_matching_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1567,10 +1571,10 @@ def test_method_matching_and_url_and_headers_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - +@pytest.mark.httpx_mock( + assert_all_responses_were_requested=False, assert_all_requests_were_expected=False +) def test_method_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1593,9 +1597,6 @@ def test_method_and_url_and_headers_and_content_not_matching( 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""" ) - # Clean up responses to avoid assertion failure - httpx_mock.reset(assert_all_responses_were_requested=False) - def test_header_as_str_tuple_list(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( @@ -1678,7 +1679,7 @@ def test_reset_is_removing_requests(httpx_mock: HTTPXMock) -> None: assert len(httpx_mock.get_requests()) == 1 - httpx_mock.reset(assert_all_responses_were_requested=False) + httpx_mock.reset() assert len(httpx_mock.get_requests()) == 0 diff --git a/tests/test_plugin.py b/tests/test_plugin.py index f821beb..2e4880d 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -43,16 +43,14 @@ def test_httpx_mock_unused_response(httpx_mock): def test_httpx_mock_unused_response_without_assertion(testdir: Testdir) -> None: """ - Unused responses should not fail test case if assert_all_responses_were_requested fixture is set to False. + Unused responses should not fail test case if + assert_all_responses_were_requested option is set to False. """ testdir.makepyfile( """ import pytest - - @pytest.fixture - def assert_all_responses_were_requested() -> bool: - return False + @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_httpx_mock_unused_response_without_assertion(httpx_mock): httpx_mock.add_response() """ @@ -87,16 +85,14 @@ def unused(*args, **kwargs): def test_httpx_mock_unused_callback_without_assertion(testdir: Testdir) -> None: """ - Unused callbacks should not fail test case if assert_all_responses_were_requested fixture is set to False. + Unused callbacks should not fail test case if + assert_all_responses_were_requested option is set to False. """ testdir.makepyfile( """ import pytest - - @pytest.fixture - def assert_all_responses_were_requested() -> bool: - return False + @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_httpx_mock_unused_callback_without_assertion(httpx_mock): def unused(*args, **kwargs): pass @@ -109,6 +105,55 @@ def unused(*args, **kwargs): result.assert_outcomes(passed=1) +def test_httpx_mock_unexpected_request(testdir: Testdir) -> None: + """ + Unexpected request should fail test case if + assert_all_requests_were_expected option is set to True (default). + """ + testdir.makepyfile( + """ + import httpx + import pytest + + def test_httpx_mock_unexpected_request(httpx_mock): + with httpx.Client() as client: + # Non mocked 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:", + "*[]", + ] + ) + + +def test_httpx_mock_unexpected_request_without_assertion(testdir: Testdir) -> None: + """ + Unexpected request should not fail test case if + assert_all_requests_were_expected option is set to False. + """ + testdir.makepyfile( + """ + import httpx + import pytest + + @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) + def test_httpx_mock_unexpected_request(httpx_mock): + with httpx.Client() as client: + # Non mocked request + with pytest.raises(httpx.TimeoutException): + 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. @@ -117,11 +162,8 @@ def test_httpx_mock_non_mocked_hosts_sync(testdir: Testdir) -> None: """ import httpx import pytest - - @pytest.fixture - def non_mocked_hosts() -> list: - return ["localhost"] + @pytest.mark.httpx_mock(non_mocked_hosts=["localhost"]) def test_httpx_mock_non_mocked_hosts_sync(httpx_mock): httpx_mock.add_response() @@ -150,12 +192,9 @@ def test_httpx_mock_non_mocked_hosts_async(testdir: Testdir) -> None: """ import httpx import pytest - - @pytest.fixture - def non_mocked_hosts() -> list: - return ["localhost"] @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() @@ -174,3 +213,22 @@ async def test_httpx_mock_non_mocked_hosts_async(httpx_mock): ) result = testdir.runpytest() result.assert_outcomes(passed=1) + + +def test_invalid_marker(testdir: Testdir) -> None: + """ + Unknown marker keyword arguments should raise a TypeError. + """ + testdir.makepyfile( + """ + import pytest + + @pytest.mark.httpx_mock(foo=123) + def test_httpx_mock_non_mocked_hosts_async(httpx_mock): + pass + + """ + ) + result = testdir.runpytest() + result.assert_outcomes(errors=1) + result.stdout.re_match_lines([r".*got an unexpected keyword argument 'foo'"])