From d2166e7cc85970b75505f2ee26b8c71a7a6c7b95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20K=C5=82oczko?= Date: Tue, 19 Mar 2024 08:47:49 +0000 Subject: [PATCH 01/23] really drop python<=3.7 support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filter all the code over `pyupgrade --py38`. Signed-off-by: Tomasz Kłoczko --- pytest_httpx/_httpx_internals.py | 3 +-- pytest_httpx/_pretty_print.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pytest_httpx/_httpx_internals.py b/pytest_httpx/_httpx_internals.py index da21797..590f871 100644 --- a/pytest_httpx/_httpx_internals.py +++ b/pytest_httpx/_httpx_internals.py @@ -30,8 +30,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/_pretty_print.py b/pytest_httpx/_pretty_print.py index 2d6f336..ddedf7c 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( - [ + 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 From 09572bb25a3db59d699eb1d922330c7d3ce35227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20K=C5=82oczko?= Date: Tue, 19 Mar 2024 08:50:29 +0000 Subject: [PATCH 02/23] mock is no longer used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomasz Kłoczko --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 841b354..cc0390e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ maintainers = [ ] keywords = [ "httpx", - "mock", "pytest", "testing", ] From 8449b1b459d6fb2401fd0f71715236271591e108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20K=C5=82oczko?= Date: Tue, 19 Mar 2024 08:57:20 +0000 Subject: [PATCH 03/23] really drop python<=3.8 support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filter all the code over `pyupgrade --py39`. Signed-off-by: Tomasz Kłoczko --- pytest_httpx/__init__.py | 4 ++-- pytest_httpx/_httpx_internals.py | 13 +++++-------- pytest_httpx/_httpx_mock.py | 3 ++- pytest_httpx/_request_matcher.py | 3 ++- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/pytest_httpx/__init__.py b/pytest_httpx/__init__.py index 3fe8071..8b59941 100644 --- a/pytest_httpx/__init__.py +++ b/pytest_httpx/__init__.py @@ -22,7 +22,7 @@ def assert_all_responses_were_requested() -> bool: @pytest.fixture -def non_mocked_hosts() -> List[str]: +def non_mocked_hosts() -> list[str]: return [] @@ -30,7 +30,7 @@ def non_mocked_hosts() -> List[str]: def httpx_mock( monkeypatch: MonkeyPatch, assert_all_responses_were_requested: bool, - non_mocked_hosts: List[str], + non_mocked_hosts: list[str], ) -> Generator[HTTPXMock, None, None]: # Ensure redirections to www hosts are handled transparently. missing_www = [ diff --git a/pytest_httpx/_httpx_internals.py b/pytest_httpx/_httpx_internals.py index 590f871..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]], ] diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 941b050..d5ca34e 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -1,6 +1,7 @@ import copy import inspect -from typing import Union, Optional, Callable, Any, Awaitable +from typing import Union, Optional, Callable, Any +from collections.abc import Awaitable import httpx 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 From 5b4540fca99790ec031a16ad3ac8bce4147a0594 Mon Sep 17 00:00:00 2001 From: Frazer McLean Date: Fri, 26 Apr 2024 16:10:53 +0200 Subject: [PATCH 04/23] Setup httpx_mock via marker instead of fixtures We want to be able to configure how the httpx_mock fixture behaves at different scopes. A pytest marker can be used at module, class, or test scope and so is what is used here. Providing a non-default value from the fixtures will emit a DeprecationWarning for each test with the exact marker it can be replaced with: =========================== warnings summary =========================== tests/test_tmp.py::test_a tests/test_tmp.py::test_b /.../pytest_httpx/__init__.py:56: DeprecationWarning: The assert_all_r esponses_were_requested and non_mocked_hosts fixtures are deprecated. Use the following marker instead: pytest.mark.httpx_mock(assert_all_responses_were_requested=False) warnings.warn( tests/test_tmp.py::test_c /.../pytest_httpx/__init__.py:56: DeprecationWarning: The assert_all_r esponses_were_requested and non_mocked_hosts fixtures are deprecated. Use the following marker instead: pytest.mark.httpx_mock(non_mocked_hosts=['example.com']) warnings.warn( Resolves #137 --- CHANGELOG.md | 14 ++++ pytest_httpx/__init__.py | 51 ++++++++++--- pytest_httpx/_httpx_mock.py | 48 ++++++++++++ tests/test_plugin.py | 148 +++++++++++++++++++++++++++++++++++- 4 files changed, 247 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d7855f..4853e13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- The `httpx_mock` fixture is now configured using a marker. + ```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): + ... + ``` +### Deprecated +- `assert_all_responses_were_requested` fixture +- `non_mocked_hosts` fixture ## [0.30.0] - 2024-02-21 ### Changed diff --git a/pytest_httpx/__init__.py b/pytest_httpx/__init__.py index 3fe8071..5cbf0c0 100644 --- a/pytest_httpx/__init__.py +++ b/pytest_httpx/__init__.py @@ -1,11 +1,12 @@ +import warnings 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,6 +17,14 @@ ) +FIXTURE_DEPRECATION_MSG = """\ +The assert_all_responses_were_requested and non_mocked_hosts fixtures are deprecated. +Use the following marker instead: + +{options!r} +""" + + @pytest.fixture def assert_all_responses_were_requested() -> bool: return True @@ -31,12 +40,26 @@ 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") + + if marker: + options = HTTPXMockOptions.from_marker(marker) + else: + deprecated_usage = not assert_all_responses_were_requested or non_mocked_hosts + options = HTTPXMockOptions( + assert_all_responses_were_requested=assert_all_responses_were_requested, + non_mocked_hosts=non_mocked_hosts, + ) + if deprecated_usage: + warnings.warn( + FIXTURE_DEPRECATION_MSG.format(options=options), DeprecationWarning + ) + + # Make sure we use options instead + del non_mocked_hosts + del assert_all_responses_were_requested mock = HTTPXMock() @@ -46,7 +69,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 +85,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 +96,12 @@ async def mocked_handle_async_request( ) yield mock - mock.reset(assert_all_responses_were_requested) + mock.reset(options.assert_all_responses_were_requested) + + +def pytest_configure(config: Config) -> None: + config.addinivalue_line( + "markers", + "httpx_mock(*, assert_all_responses_were_requested=True, " + "non_mocked_hosts=[]): Configure httpx_mock fixture.", + ) diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 941b050..2e88fd1 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -1,8 +1,11 @@ import copy import inspect +from functools import cached_property +from operator import methodcaller from typing import Union, Optional, Callable, Any, Awaitable import httpx +from pytest import Mark from pytest_httpx import _httpx_internals from pytest_httpx._pretty_print import RequestDescription @@ -266,6 +269,51 @@ def _reset_callbacks(self) -> list[_RequestMatcher]: return callbacks_not_executed +class HTTPXMockOptions: + def __init__( + self, + *, + assert_all_responses_were_requested: bool = True, + non_mocked_hosts: Optional[list[str]] = None, + ) -> None: + if non_mocked_hosts is None: + non_mocked_hosts = [] + + self.assert_all_responses_were_requested = assert_all_responses_were_requested + + # The original non_mocked_hosts list is shown in the __repr__, see the + # non_mocked_hosts property for more. + self._non_mocked_hosts = non_mocked_hosts + + @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) + + @cached_property + def non_mocked_hosts(self) -> list[str]: + # Ensure redirections to www hosts are handled transparently. + missing_www = [ + f"www.{host}" + for host in self._non_mocked_hosts + if not host.startswith("www.") + ] + return [*self._non_mocked_hosts, *missing_www] + + def __repr__(self) -> str: + kwargs = [] + if not self.assert_all_responses_were_requested: + kwargs.append( + f"assert_all_responses_were_requested={self.assert_all_responses_were_requested!r}" + ) + if self._non_mocked_hosts: + kwargs.append(f"non_mocked_hosts={self._non_mocked_hosts!r}") + return f"pytest.mark.httpx_mock(" + ", ".join(kwargs) + ")" + + def _unread(response: httpx.Response) -> httpx.Response: # Allow to read the response on client side response.is_stream_consumed = False diff --git a/tests/test_plugin.py b/tests/test_plugin.py index f821beb..2b29949 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -41,7 +41,9 @@ def test_httpx_mock_unused_response(httpx_mock): ) -def test_httpx_mock_unused_response_without_assertion(testdir: Testdir) -> None: +def test_httpx_mock_unused_response_without_assertion_via_fixture( + testdir: Testdir, +) -> None: """ Unused responses should not fail test case if assert_all_responses_were_requested fixture is set to False. """ @@ -59,6 +61,28 @@ def test_httpx_mock_unused_response_without_assertion(httpx_mock): ) result = testdir.runpytest() result.assert_outcomes(passed=1) + # Our deprecation warning should show how to configure an equivalent marker + result.stdout.re_match_lines( + [r".*pytest\.mark\.httpx_mock\(assert_all_responses_were_requested=False\)"] + ) + + +def test_httpx_mock_unused_response_without_assertion(testdir: Testdir) -> None: + """ + Unused responses should not fail test case if + assert_all_responses_were_requested option is set to False. + """ + testdir.makepyfile( + """ + import pytest + + @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) + def test_httpx_mock_unused_response_without_assertion(httpx_mock): + httpx_mock.add_response() + """ + ) + result = testdir.runpytest() + result.assert_outcomes(passed=1) def test_httpx_mock_unused_callback(testdir: Testdir) -> None: @@ -85,7 +109,9 @@ def unused(*args, **kwargs): ) -def test_httpx_mock_unused_callback_without_assertion(testdir: Testdir) -> None: +def test_httpx_mock_unused_callback_without_assertion_via_fixture( + testdir: Testdir, +) -> None: """ Unused callbacks should not fail test case if assert_all_responses_were_requested fixture is set to False. """ @@ -107,9 +133,35 @@ def unused(*args, **kwargs): ) result = testdir.runpytest() result.assert_outcomes(passed=1) + # Our deprecation warning should show how to configure an equivalent marker + result.stdout.re_match_lines( + [r".*pytest\.mark\.httpx_mock\(assert_all_responses_were_requested=False\)"] + ) -def test_httpx_mock_non_mocked_hosts_sync(testdir: Testdir) -> None: +def test_httpx_mock_unused_callback_without_assertion(testdir: Testdir) -> None: + """ + Unused callbacks should not fail test case if + assert_all_responses_were_requested option is set to False. + """ + testdir.makepyfile( + """ + import pytest + + @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 + + httpx_mock.add_callback(unused) + + """ + ) + result = testdir.runpytest() + result.assert_outcomes(passed=1) + + +def test_httpx_mock_non_mocked_hosts_sync_via_fixture(testdir: Testdir) -> None: """ Non mocked hosts should go through while other requests should be mocked. """ @@ -140,9 +192,43 @@ def test_httpx_mock_non_mocked_hosts_sync(httpx_mock): ) result = testdir.runpytest() result.assert_outcomes(passed=1) + # Our deprecation warning should show how to configure an equivalent marker + result.stdout.re_match_lines( + [r".*pytest\.mark\.httpx_mock\(non_mocked_hosts=\['localhost'\]\)"] + ) -def test_httpx_mock_non_mocked_hosts_async(testdir: Testdir) -> None: +def test_httpx_mock_non_mocked_hosts_sync(testdir: Testdir) -> None: + """ + Non mocked hosts should go through while other requests should be mocked. + """ + testdir.makepyfile( + """ + import httpx + import pytest + + @pytest.mark.httpx_mock(non_mocked_hosts=["localhost"]) + def test_httpx_mock_non_mocked_hosts_sync(httpx_mock): + httpx_mock.add_response() + + with httpx.Client() as client: + # Mocked request + client.get("https://foo.tld") + + # Non mocked request + with pytest.raises(httpx.ConnectError): + client.get("https://localhost:5005") + + # Assert that a single request was mocked + assert len(httpx_mock.get_requests()) == 1 + + """ + ) + result = testdir.runpytest() + result.assert_outcomes(passed=1) + + +def test_httpx_mock_non_mocked_hosts_async_via_fixture(testdir: Testdir) -> None: """ Non mocked hosts should go through while other requests should be mocked. """ @@ -174,3 +260,57 @@ async def test_httpx_mock_non_mocked_hosts_async(httpx_mock): ) result = testdir.runpytest() result.assert_outcomes(passed=1) + # Our deprecation warning should show how to configure an equivalent marker + result.stdout.re_match_lines( + [r".*pytest\.mark\.httpx_mock\(non_mocked_hosts=\['localhost'\]\)"] + ) + + +def test_httpx_mock_non_mocked_hosts_async(testdir: Testdir) -> None: + """ + Non mocked hosts should go through while other requests should be mocked. + """ + testdir.makepyfile( + """ + import httpx + import pytest + + @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() + + async with httpx.AsyncClient() as client: + # Mocked request + await client.get("https://foo.tld") + + # Non mocked request + with pytest.raises(httpx.ConnectError): + await client.get("https://localhost:5005") + + # Assert that a single request was mocked + assert len(httpx_mock.get_requests()) == 1 + + """ + ) + 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".*unexpected keyword argument 'foo'"]) From ee369770fb635c44e9f7f49583edc35872ff907c Mon Sep 17 00:00:00 2001 From: Frazer McLean Date: Fri, 26 Apr 2024 16:31:31 +0200 Subject: [PATCH 05/23] Update README for httpx_mock marker --- README.md | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f463cf2..10930e8 100644 --- a/README.md +++ b/README.md @@ -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). @@ -650,14 +658,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 +678,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() From 5f8a33c5ce234c44125977feccd55f144f453944 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 24 May 2024 08:34:25 +0200 Subject: [PATCH 06/23] Bug fix to allow multiple builds of package The [tool.setuptools.packages.find] section in 'pyproject.toml' was removed because it didn't affect the generated wheel, as the wheel still excluded the 'tests' directory with this section removed. Including [tool.setuptools.packages.find] caused issues with multiple builds of the package. This led to the creation of infinite nested 'build/lib/build/lib' directories. By removing this section, the default behaviour now ignores 'build*' directories, preventing these problems. For more details, refer to the related discussion on the setuptools: https://github.com/pypa/setuptools/issues/4076 --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 841b354..ba922d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,8 +59,5 @@ testing = [ [project.entry-points.pytest11] pytest_httpx = "pytest_httpx" -[tool.setuptools.packages.find] -exclude = ["tests*"] - [tool.setuptools.dynamic] version = {attr = "pytest_httpx.version.__version__"} From 210b819dbb11e700c8ddc6c93d53f125f3905588 Mon Sep 17 00:00:00 2001 From: Colin-b Date: Thu, 19 Sep 2024 22:27:25 +0200 Subject: [PATCH 07/23] Drop pytest 7 support --- .github/workflows/test.yml | 2 -- CHANGELOG.md | 2 ++ pyproject.toml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6d99a8b..5b163c9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,6 @@ jobs: strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12'] - pytest-major-version: ['7', '8'] 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/CHANGELOG.md b/CHANGELOG.md index 5d7855f..5cd255a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Removed +- `pytest` `7` is not supported anymore (`pytest` `8` has been out for 9 months already). ## [0.30.0] - 2024-02-21 ### Changed diff --git a/pyproject.toml b/pyproject.toml index cc0390e..90909b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ classifiers = [ ] dependencies = [ "httpx==0.27.*", - "pytest>=7,<9", + "pytest==8.*", ] dynamic = ["version"] @@ -50,9 +50,9 @@ 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] From 1d135ad99a89dfc093155626e9ead88ce376efa4 Mon Sep 17 00:00:00 2001 From: Colin-b Date: Thu, 19 Sep 2024 22:31:40 +0200 Subject: [PATCH 08/23] Update black --- .pre-commit-config.yaml | 2 +- pytest_httpx/_pretty_print.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) 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/pytest_httpx/_pretty_print.py b/pytest_httpx/_pretty_print.py index ddedf7c..f79fd1e 100644 --- a/pytest_httpx/_pretty_print.py +++ b/pytest_httpx/_pretty_print.py @@ -18,11 +18,11 @@ def __init__( headers_encoding = request.headers.encoding 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 + # 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( [ From fa12c0c60bc402ddd889d8f98eb90320500b2e07 Mon Sep 17 00:00:00 2001 From: Colin-b Date: Thu, 19 Sep 2024 23:18:36 +0200 Subject: [PATCH 09/23] Replace fixtures by marker options --- CHANGELOG.md | 7 +- pytest_httpx/__init__.py | 40 +---------- pytest_httpx/_httpx_mock.py | 10 --- tests/test_plugin.py | 139 ++---------------------------------- 4 files changed, 8 insertions(+), 188 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36b69d7..247d21a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- The `httpx_mock` fixture is now configured using a marker. +- 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) @@ -16,12 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 def test_foo(httpx_mock): ... ``` -### Deprecated -- `assert_all_responses_were_requested` fixture -- `non_mocked_hosts` fixture ### 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 diff --git a/pytest_httpx/__init__.py b/pytest_httpx/__init__.py index 35535ab..77b17ca 100644 --- a/pytest_httpx/__init__.py +++ b/pytest_httpx/__init__.py @@ -1,6 +1,4 @@ -import warnings from collections.abc import Generator -from typing import List import httpx import pytest @@ -17,49 +15,13 @@ ) -FIXTURE_DEPRECATION_MSG = """\ -The assert_all_responses_were_requested and non_mocked_hosts fixtures are deprecated. -Use the following marker instead: - -{options!r} -""" - - -@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]: marker = request.node.get_closest_marker("httpx_mock") - - if marker: - options = HTTPXMockOptions.from_marker(marker) - else: - deprecated_usage = not assert_all_responses_were_requested or non_mocked_hosts - options = HTTPXMockOptions( - assert_all_responses_were_requested=assert_all_responses_were_requested, - non_mocked_hosts=non_mocked_hosts, - ) - if deprecated_usage: - warnings.warn( - FIXTURE_DEPRECATION_MSG.format(options=options), DeprecationWarning - ) - - # Make sure we use options instead - del non_mocked_hosts - del assert_all_responses_were_requested + options = HTTPXMockOptions.from_marker(marker) if marker else HTTPXMockOptions() mock = HTTPXMock() diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index aab73a0..1c6d2c2 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -304,16 +304,6 @@ def non_mocked_hosts(self) -> list[str]: ] return [*self._non_mocked_hosts, *missing_www] - def __repr__(self) -> str: - kwargs = [] - if not self.assert_all_responses_were_requested: - kwargs.append( - f"assert_all_responses_were_requested={self.assert_all_responses_were_requested!r}" - ) - if self._non_mocked_hosts: - kwargs.append(f"non_mocked_hosts={self._non_mocked_hosts!r}") - return f"pytest.mark.httpx_mock(" + ", ".join(kwargs) + ")" - def _unread(response: httpx.Response) -> httpx.Response: # Allow to read the response on client side diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 2b29949..17b9c30 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -41,32 +41,6 @@ def test_httpx_mock_unused_response(httpx_mock): ) -def test_httpx_mock_unused_response_without_assertion_via_fixture( - testdir: Testdir, -) -> None: - """ - Unused responses should not fail test case if assert_all_responses_were_requested fixture is set to False. - """ - testdir.makepyfile( - """ - import pytest - - @pytest.fixture - def assert_all_responses_were_requested() -> bool: - return False - - def test_httpx_mock_unused_response_without_assertion(httpx_mock): - httpx_mock.add_response() - """ - ) - result = testdir.runpytest() - result.assert_outcomes(passed=1) - # Our deprecation warning should show how to configure an equivalent marker - result.stdout.re_match_lines( - [r".*pytest\.mark\.httpx_mock\(assert_all_responses_were_requested=False\)"] - ) - - def test_httpx_mock_unused_response_without_assertion(testdir: Testdir) -> None: """ Unused responses should not fail test case if @@ -109,36 +83,6 @@ def unused(*args, **kwargs): ) -def test_httpx_mock_unused_callback_without_assertion_via_fixture( - testdir: Testdir, -) -> None: - """ - Unused callbacks should not fail test case if assert_all_responses_were_requested fixture is set to False. - """ - testdir.makepyfile( - """ - import pytest - - @pytest.fixture - def assert_all_responses_were_requested() -> bool: - return False - - def test_httpx_mock_unused_callback_without_assertion(httpx_mock): - def unused(*args, **kwargs): - pass - - httpx_mock.add_callback(unused) - - """ - ) - result = testdir.runpytest() - result.assert_outcomes(passed=1) - # Our deprecation warning should show how to configure an equivalent marker - result.stdout.re_match_lines( - [r".*pytest\.mark\.httpx_mock\(assert_all_responses_were_requested=False\)"] - ) - - def test_httpx_mock_unused_callback_without_assertion(testdir: Testdir) -> None: """ Unused callbacks should not fail test case if @@ -161,43 +105,6 @@ def unused(*args, **kwargs): result.assert_outcomes(passed=1) -def test_httpx_mock_non_mocked_hosts_sync_via_fixture(testdir: Testdir) -> None: - """ - Non mocked hosts should go through while other requests should be mocked. - """ - testdir.makepyfile( - """ - import httpx - import pytest - - @pytest.fixture - def non_mocked_hosts() -> list: - return ["localhost"] - - def test_httpx_mock_non_mocked_hosts_sync(httpx_mock): - httpx_mock.add_response() - - with httpx.Client() as client: - # Mocked request - client.get("https://foo.tld") - - # Non mocked request - with pytest.raises(httpx.ConnectError): - client.get("https://localhost:5005") - - # Assert that a single request was mocked - assert len(httpx_mock.get_requests()) == 1 - - """ - ) - result = testdir.runpytest() - result.assert_outcomes(passed=1) - # Our deprecation warning should show how to configure an equivalent marker - result.stdout.re_match_lines( - [r".*pytest\.mark\.httpx_mock\(non_mocked_hosts=\['localhost'\]\)"] - ) - - def test_httpx_mock_non_mocked_hosts_sync(testdir: Testdir) -> None: """ Non mocked hosts should go through while other requests should be mocked. @@ -211,12 +118,12 @@ def test_httpx_mock_non_mocked_hosts_sync(testdir: Testdir) -> None: def test_httpx_mock_non_mocked_hosts_sync(httpx_mock): httpx_mock.add_response() - with httpx.Client() as client: + with httpx.Client(timeout=httpx.Timeout(None, connect=0.1)) as client: # Mocked request client.get("https://foo.tld") # Non mocked request - with pytest.raises(httpx.ConnectError): + with pytest.raises(httpx.ConnectTimeout): client.get("https://localhost:5005") # Assert that a single request was mocked @@ -228,44 +135,6 @@ def test_httpx_mock_non_mocked_hosts_sync(httpx_mock): result.assert_outcomes(passed=1) -def test_httpx_mock_non_mocked_hosts_async_via_fixture(testdir: Testdir) -> None: - """ - Non mocked hosts should go through while other requests should be mocked. - """ - testdir.makepyfile( - """ - import httpx - import pytest - - @pytest.fixture - def non_mocked_hosts() -> list: - return ["localhost"] - - @pytest.mark.asyncio - async def test_httpx_mock_non_mocked_hosts_async(httpx_mock): - httpx_mock.add_response() - - async with httpx.AsyncClient() as client: - # Mocked request - await client.get("https://foo.tld") - - # Non mocked request - with pytest.raises(httpx.ConnectError): - await client.get("https://localhost:5005") - - # Assert that a single request was mocked - assert len(httpx_mock.get_requests()) == 1 - - """ - ) - result = testdir.runpytest() - result.assert_outcomes(passed=1) - # Our deprecation warning should show how to configure an equivalent marker - result.stdout.re_match_lines( - [r".*pytest\.mark\.httpx_mock\(non_mocked_hosts=\['localhost'\]\)"] - ) - - def test_httpx_mock_non_mocked_hosts_async(testdir: Testdir) -> None: """ Non mocked hosts should go through while other requests should be mocked. @@ -280,12 +149,12 @@ def test_httpx_mock_non_mocked_hosts_async(testdir: Testdir) -> None: async def test_httpx_mock_non_mocked_hosts_async(httpx_mock): httpx_mock.add_response() - async with httpx.AsyncClient() as client: + async with httpx.AsyncClient(timeout=httpx.Timeout(None, connect=0.1)) as client: # Mocked request await client.get("https://foo.tld") # Non mocked request - with pytest.raises(httpx.ConnectError): + with pytest.raises(httpx.ConnectTimeout): await client.get("https://localhost:5005") # Assert that a single request was mocked From fd35d03f71abee5dfbbbfc4a601076e67ea34f25 Mon Sep 17 00:00:00 2001 From: Colin-b Date: Thu, 19 Sep 2024 23:19:12 +0200 Subject: [PATCH 10/23] Document the replacement as a change --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 247d21a..56c2174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -### Added +### Changed - 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 From ecc075ef760e3b83f0bb915d1de6d08ad0a1c2fc Mon Sep 17 00:00:00 2001 From: Colin-b Date: Thu, 19 Sep 2024 23:35:04 +0200 Subject: [PATCH 11/23] error is not the same when running locally and on CI --- tests/test_plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 17b9c30..65d1108 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -118,12 +118,12 @@ def test_httpx_mock_non_mocked_hosts_sync(testdir: Testdir) -> None: def test_httpx_mock_non_mocked_hosts_sync(httpx_mock): httpx_mock.add_response() - with httpx.Client(timeout=httpx.Timeout(None, connect=0.1)) as client: + with httpx.Client() as client: # Mocked request client.get("https://foo.tld") # Non mocked request - with pytest.raises(httpx.ConnectTimeout): + with pytest.raises(httpx.ConnectError): client.get("https://localhost:5005") # Assert that a single request was mocked @@ -149,12 +149,12 @@ def test_httpx_mock_non_mocked_hosts_async(testdir: Testdir) -> None: async def test_httpx_mock_non_mocked_hosts_async(httpx_mock): httpx_mock.add_response() - async with httpx.AsyncClient(timeout=httpx.Timeout(None, connect=0.1)) as client: + async with httpx.AsyncClient() as client: # Mocked request await client.get("https://foo.tld") # Non mocked request - with pytest.raises(httpx.ConnectTimeout): + with pytest.raises(httpx.ConnectError): await client.get("https://localhost:5005") # Assert that a single request was mocked From e3653b9e2f6507b22b831f3ca2961e47852b9e0f Mon Sep 17 00:00:00 2001 From: Colin-b Date: Thu, 19 Sep 2024 23:35:21 +0200 Subject: [PATCH 12/23] Remove asyncio warning --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 021303b..23bfe63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,3 +60,7 @@ pytest_httpx = "pytest_httpx" [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" From ba1e11bbffd919ae8e4d3062f09b1c488a316523 Mon Sep 17 00:00:00 2001 From: Colin-b Date: Thu, 19 Sep 2024 23:35:32 +0200 Subject: [PATCH 13/23] Document options --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56c2174..512ca02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 def test_foo(httpx_mock): ... ``` + - The following options are available: + - `assert_all_responses_were_requested` (boolean), defaulting to `True`. + - `non_mocked_hosts` (iterable), defaulting to an empty list, meaning all hosts are mocked. ### Removed - `pytest` `7` is not supported anymore (`pytest` `8` has been out for 9 months already). From ff0e66d3c67d5fb5a8eb89315b997b1dfb7ebca0 Mon Sep 17 00:00:00 2001 From: Colin-b Date: Thu, 19 Sep 2024 23:42:42 +0200 Subject: [PATCH 14/23] test 3.13 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5b163c9..20b1f19 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 From a6412de30822c001e101af9fb36e332f8ea5df0c Mon Sep 17 00:00:00 2001 From: Colin-b Date: Thu, 19 Sep 2024 23:43:24 +0200 Subject: [PATCH 15/23] add rc2 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 20b1f19..f0caf98 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13.0-rc.2'] steps: - uses: actions/checkout@v4 From 5951f26b6505e9868a87777a9ef1605c1eaee9fa Mon Sep 17 00:00:00 2001 From: Colin-b Date: Thu, 19 Sep 2024 23:53:40 +0200 Subject: [PATCH 16/23] Avoid duplicated code for requests not matched --- pytest_httpx/_httpx_mock.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 1c6d2c2..ae954ec 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -2,7 +2,7 @@ import inspect from functools import cached_property from operator import methodcaller -from typing import Union, Optional, Callable, Any +from typing import Union, Optional, Callable, Any, NoReturn from collections.abc import Awaitable import httpx @@ -139,10 +139,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, @@ -160,6 +157,13 @@ 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: raise httpx.TimeoutException( self._explain_that_no_response_was_found(real_transport, request), request=request, From 80fe905bf984396dcfc208b5f25694165b82ae98 Mon Sep 17 00:00:00 2001 From: Colin-b Date: Thu, 19 Sep 2024 23:56:54 +0200 Subject: [PATCH 17/23] Store unmatched requests --- pytest_httpx/_httpx_mock.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index ae954ec..01eb8b2 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -29,6 +29,7 @@ def __init__(self) -> None: ], ] ] = [] + self._requests_not_matched: list[httpx.Request] = [] def add_response( self, @@ -164,6 +165,7 @@ def _request_not_matched( 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, From 9cc823b8dbb71fffdb0c63089df87bb247bc883e Mon Sep 17 00:00:00 2001 From: Colin-b Date: Fri, 20 Sep 2024 00:34:54 +0200 Subject: [PATCH 18/23] Reduce reset usage in tests --- pytest_httpx/_httpx_mock.py | 1 + tests/test_httpx_async.py | 120 +++++++++--------------------------- tests/test_httpx_sync.py | 120 +++++++++--------------------------- 3 files changed, 61 insertions(+), 180 deletions(-) diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 01eb8b2..793ee96 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -257,6 +257,7 @@ 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 + # TODO reset should not expose any parameter def reset(self, assert_all_responses_were_requested: bool) -> None: self._requests.clear() not_called = self._reset_callbacks() diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py index 89607ff..c96dff8 100644 --- a/tests/test_httpx_async.py +++ b/tests/test_httpx_async.py @@ -61,6 +61,7 @@ async def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) async def test_url_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url") @@ -73,11 +74,9 @@ 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) 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 +90,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 +104,7 @@ async def test_method_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) async def test_method_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get") @@ -120,9 +117,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 +1101,7 @@ async def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) async def test_multi_value_headers_not_matching_single_value_issued( httpx_mock: HTTPXMock, ) -> None: @@ -1127,11 +1122,9 @@ 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) async def test_multi_value_headers_not_matching_multi_value_issued( httpx_mock: HTTPXMock, ) -> None: @@ -1152,11 +1145,9 @@ 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) 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 +1162,9 @@ 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) async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1194,11 +1183,9 @@ 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) async def test_url_not_matching_upper_case_headers_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1216,9 +1203,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 +1223,7 @@ async def test_proxy_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) async def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy") @@ -1251,11 +1236,9 @@ 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) async def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy") @@ -1268,9 +1251,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 +1317,7 @@ async def test_request_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) async def test_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_content=b"This is the body") @@ -1349,9 +1330,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 +1350,7 @@ async def test_json_partial_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) async def test_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) @@ -1384,11 +1363,9 @@ 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) async def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_json={"a": 1, "b": 2}, @@ -1404,11 +1381,9 @@ 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) async def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) @@ -1421,9 +1396,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 +1410,7 @@ async def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) async def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1456,11 +1429,9 @@ 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) async def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1479,11 +1450,9 @@ 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) async def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1502,9 +1471,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 +1486,7 @@ 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) async def test_headers_not_matching_and_url_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1541,11 +1508,9 @@ 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) async def test_url_and_headers_not_matching_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1567,11 +1532,9 @@ 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) async def test_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1593,11 +1556,9 @@ 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) async def test_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1619,11 +1580,9 @@ 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) async def test_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1645,11 +1604,9 @@ 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) async def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url2", @@ -1669,9 +1626,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 +1644,7 @@ async def test_method_and_url_and_headers_and_content_matching( @pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) async def test_headers_not_matching_and_method_and_url_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1712,11 +1667,9 @@ 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) async def test_url_and_headers_not_matching_and_method_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1739,11 +1692,9 @@ 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) async def test_method_and_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1766,11 +1717,9 @@ 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) async def test_method_and_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1793,11 +1742,9 @@ 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) async def test_method_and_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1820,11 +1767,9 @@ 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) async def test_method_matching_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1847,11 +1792,9 @@ 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) async def test_method_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1874,9 +1817,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: diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py index e92e2be..ad71258 100644 --- a/tests/test_httpx_sync.py +++ b/tests/test_httpx_sync.py @@ -53,6 +53,7 @@ def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_url_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url") @@ -65,10 +66,8 @@ 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) 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 +81,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 +93,7 @@ def test_method_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_method_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get") @@ -109,9 +106,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 +134,7 @@ 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) def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( method="GET", @@ -155,9 +150,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 +872,7 @@ def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_multi_value_headers_not_matching_single_value_issued( httpx_mock: HTTPXMock, ) -> None: @@ -900,10 +893,8 @@ 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) def test_multi_value_headers_not_matching_multi_value_issued( httpx_mock: HTTPXMock, ) -> None: @@ -924,10 +915,8 @@ 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) 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 +931,8 @@ 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) def test_headers_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -964,9 +951,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 +968,7 @@ def test_proxy_matching(httpx_mock: HTTPXMock) -> None: assert response.read() == b"" +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy") @@ -996,10 +981,8 @@ 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) def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy") @@ -1012,9 +995,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 +1052,7 @@ 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) def test_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_content=b"This is the body") @@ -1084,9 +1065,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 +1092,7 @@ def test_json_partial_matching(httpx_mock: HTTPXMock) -> None: assert response.read() == b"" +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) @@ -1126,10 +1105,8 @@ 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) def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_json={"a": 1, "b": 2}, @@ -1145,10 +1122,8 @@ 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) def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) @@ -1161,9 +1136,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 +1148,7 @@ def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1194,10 +1167,8 @@ 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) def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1216,10 +1187,8 @@ 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) def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ @@ -1238,9 +1207,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 +1220,7 @@ 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) def test_headers_not_matching_and_url_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1275,10 +1242,8 @@ 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) def test_url_and_headers_not_matching_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1300,10 +1265,8 @@ 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) def test_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1325,10 +1288,8 @@ 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) def test_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1350,10 +1311,8 @@ 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) def test_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1375,10 +1334,8 @@ 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) def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url2", @@ -1398,9 +1355,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 +1369,7 @@ 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) def test_headers_not_matching_and_method_and_url_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1437,10 +1392,8 @@ 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) def test_url_and_headers_not_matching_and_method_and_content_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1463,10 +1416,8 @@ 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) def test_method_and_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1489,10 +1440,8 @@ 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) def test_method_and_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1515,10 +1464,8 @@ 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) def test_method_and_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1541,10 +1488,8 @@ 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) def test_method_matching_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1567,10 +1512,8 @@ 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) def test_method_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: @@ -1593,9 +1536,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( From f7059286db747ab79b06cf3f75b8fa13f4c62957 Mon Sep 17 00:00:00 2001 From: Colin-b Date: Fri, 20 Sep 2024 00:57:25 +0200 Subject: [PATCH 19/23] Split reset and assertions --- CHANGELOG.md | 1 + pytest_httpx/__init__.py | 5 +- pytest_httpx/_httpx_mock.py | 95 ++++++++++++++++++------------------- tests/test_httpx_async.py | 2 +- tests/test_httpx_sync.py | 2 +- 5 files changed, 54 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 512ca02..1ba2912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The following options are available: - `assert_all_responses_were_requested` (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). diff --git a/pytest_httpx/__init__.py b/pytest_httpx/__init__.py index 77b17ca..2837171 100644 --- a/pytest_httpx/__init__.py +++ b/pytest_httpx/__init__.py @@ -58,7 +58,10 @@ async def mocked_handle_async_request( ) yield mock - mock.reset(options.assert_all_responses_were_requested) + try: + mock._assert_options(options) + finally: + mock.reset() def pytest_configure(config: Config) -> None: diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 793ee96..78b0edb 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -13,6 +13,41 @@ from pytest_httpx._request_matcher import _RequestMatcher +class HTTPXMockOptions: + def __init__( + self, + *, + assert_all_responses_were_requested: bool = True, + non_mocked_hosts: Optional[list[str]] = None, + ) -> None: + if non_mocked_hosts is None: + non_mocked_hosts = [] + + self.assert_all_responses_were_requested = assert_all_responses_were_requested + + # The original non_mocked_hosts list is shown in the __repr__, see the + # non_mocked_hosts property for more. + self._non_mocked_hosts = non_mocked_hosts + + @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) + + @cached_property + def non_mocked_hosts(self) -> list[str]: + # Ensure redirections to www hosts are handled transparently. + missing_www = [ + f"www.{host}" + for host in self._non_mocked_hosts + if not host.startswith("www.") + ] + return [*self._non_mocked_hosts, *missing_www] + + class HTTPXMock: def __init__(self) -> None: self._requests: list[ @@ -257,60 +292,24 @@ 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 - # TODO reset should not expose any parameter - 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 - - -class HTTPXMockOptions: - def __init__( - self, - *, - assert_all_responses_were_requested: bool = True, - non_mocked_hosts: Optional[list[str]] = None, - ) -> None: - if non_mocked_hosts is None: - non_mocked_hosts = [] - - self.assert_all_responses_were_requested = assert_all_responses_were_requested - - # The original non_mocked_hosts list is shown in the __repr__, see the - # non_mocked_hosts property for more. - self._non_mocked_hosts = non_mocked_hosts - - @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) - - @cached_property - def non_mocked_hosts(self) -> list[str]: - # Ensure redirections to www hosts are handled transparently. - missing_www = [ - f"www.{host}" - for host in self._non_mocked_hosts - if not host.startswith("www.") - ] - return [*self._non_mocked_hosts, *missing_www] - def _unread(response: httpx.Response) -> httpx.Response: # Allow to read the response on client side diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py index c96dff8..b3ef121 100644 --- a/tests/test_httpx_async.py +++ b/tests/test_httpx_async.py @@ -1920,7 +1920,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 ad71258..3348cf4 100644 --- a/tests/test_httpx_sync.py +++ b/tests/test_httpx_sync.py @@ -1618,7 +1618,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 From 227d6841a3c2e8e5cab9e6efae250ef504196d41 Mon Sep 17 00:00:00 2001 From: Colin-b Date: Fri, 20 Sep 2024 01:26:02 +0200 Subject: [PATCH 20/23] Allow to assert that all requests were expected --- CHANGELOG.md | 6 +++++ README.md | 15 ++++++++++++ pytest_httpx/_httpx_mock.py | 30 +++++++++++------------ tests/test_plugin.py | 49 +++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ba2912..5076319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ``` - The following options are available: - `assert_all_responses_were_requested` (boolean), defaulting to `True`. + - `assert_all_requests_were_expected` (boolean), defaulting to `False`. - `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). +### Added +- It is now possible to ensure that all requests were expected. Use `pytest.mark.httpx_mock(assert_all_requests_were_expected=True)`. + - This is especially useful if your code is swallowing exceptions that are raised by `httpx_mock` when receiving an unexpected request. + - Note that the default behavior will change to `True` in the next release. Set it to `False` if you want to keep previous behavior. + ### 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. diff --git a/README.md b/README.md index 10930e8..4d097c6 100644 --- a/README.md +++ b/README.md @@ -572,6 +572,21 @@ 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 send an `httpx.TimeoutException` in case no response can be found. +However, should your test swallow exceptions, you can use the `httpx_mock` marker to ensure that only expected requests have been issued: + +```python +import pytest + +# For whole module +pytestmark = pytest.mark.httpx_mock(assert_all_requests_were_expected=True) + +# For specific test +@pytest.mark.httpx_mock(assert_all_requests_were_expected=True) +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: diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 78b0edb..6c42b3b 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -1,6 +1,5 @@ import copy import inspect -from functools import cached_property from operator import methodcaller from typing import Union, Optional, Callable, Any, NoReturn from collections.abc import Awaitable @@ -18,16 +17,20 @@ def __init__( self, *, assert_all_responses_were_requested: bool = True, + assert_all_requests_were_expected: 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 + if non_mocked_hosts is None: non_mocked_hosts = [] - self.assert_all_responses_were_requested = assert_all_responses_were_requested - - # The original non_mocked_hosts list is shown in the __repr__, see the - # non_mocked_hosts property for more. - self._non_mocked_hosts = 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": @@ -37,16 +40,6 @@ def from_marker(cls, marker: Mark) -> "HTTPXMockOptions": __tracebackhide__ = methodcaller("errisinstance", TypeError) return cls(**marker.kwargs) - @cached_property - def non_mocked_hosts(self) -> list[str]: - # Ensure redirections to www hosts are handled transparently. - missing_www = [ - f"www.{host}" - for host in self._non_mocked_hosts - if not host.startswith("www.") - ] - return [*self._non_mocked_hosts, *missing_www] - class HTTPXMock: def __init__(self) -> None: @@ -310,6 +303,11 @@ def _assert_options(self, options: HTTPXMockOptions) -> None: not callbacks_not_executed ), f"The following responses are mocked but not requested:\n{matchers_description}" + 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: # Allow to read the response on client side diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 65d1108..791e3a0 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -105,6 +105,55 @@ def unused(*args, **kwargs): result.assert_outcomes(passed=1) +def test_httpx_mock_unexpected_request(testdir: Testdir) -> None: + """ + Unexpected request should not fail test case if + assert_all_requests_were_expected option is set to False (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(passed=1) + + +def test_httpx_mock_unexpected_request_with_assertion(testdir: Testdir) -> None: + """ + Unexpected request should fail test case if + assert_all_requests_were_expected option is set to True. + """ + testdir.makepyfile( + """ + import httpx + import pytest + + @pytest.mark.httpx_mock(assert_all_requests_were_expected=True) + 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_non_mocked_hosts_sync(testdir: Testdir) -> None: """ Non mocked hosts should go through while other requests should be mocked. From 5cf03e3fca3f09c15442ced13c7fdb319859614a Mon Sep 17 00:00:00 2001 From: Colin-b Date: Fri, 20 Sep 2024 09:53:12 +0200 Subject: [PATCH 21/23] Default behavior is now to not rely on the code to fail on raised Timeout, but instead fail at teardown if unexpected requests were issued --- CHANGELOG.md | 9 +-- README.md | 8 +-- pytest_httpx/__init__.py | 3 +- pytest_httpx/_httpx_mock.py | 6 +- tests/test_httpx_async.py | 121 +++++++++++++++++++++++++++--------- tests/test_httpx_sync.py | 121 +++++++++++++++++++++++++++--------- tests/test_plugin.py | 30 ++++----- 7 files changed, 207 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5076319..aed2284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### 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 @@ -18,15 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ``` - The following options are available: - `assert_all_responses_were_requested` (boolean), defaulting to `True`. - - `assert_all_requests_were_expected` (boolean), defaulting to `False`. + - `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). -### Added -- It is now possible to ensure that all requests were expected. Use `pytest.mark.httpx_mock(assert_all_requests_were_expected=True)`. - - This is especially useful if your code is swallowing exceptions that are raised by `httpx_mock` when receiving an unexpected request. - - Note that the default behavior will change to `True` in the next release. Set it to `False` if you want to keep previous behavior. - ### 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. diff --git a/README.md b/README.md index 4d097c6..3a60d1f 100644 --- a/README.md +++ b/README.md @@ -560,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): @@ -572,17 +573,16 @@ 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 send an `httpx.TimeoutException` in case no response can be found. -However, should your test swallow exceptions, you can use the `httpx_mock` marker to ensure that only expected requests have been issued: +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=True) +pytestmark = pytest.mark.httpx_mock(assert_all_requests_were_expected=False) # For specific test -@pytest.mark.httpx_mock(assert_all_requests_were_expected=True) +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_something(httpx_mock): ... ``` diff --git a/pytest_httpx/__init__.py b/pytest_httpx/__init__.py index 2837171..28f400d 100644 --- a/pytest_httpx/__init__.py +++ b/pytest_httpx/__init__.py @@ -67,6 +67,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, " - "non_mocked_hosts=[]): Configure httpx_mock fixture.", + "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_mock.py b/pytest_httpx/_httpx_mock.py index 6c42b3b..33ac08b 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -17,7 +17,7 @@ def __init__( self, *, assert_all_responses_were_requested: bool = True, - assert_all_requests_were_expected: bool = False, + 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 @@ -34,9 +34,7 @@ def __init__( @classmethod def from_marker(cls, marker: Mark) -> "HTTPXMockOptions": - """Initialise from a marker so that the marker kwargs raise an error if - incorrect. - """ + """Initialise from a marker so that the marker kwargs raise an error if incorrect.""" __tracebackhide__ = methodcaller("errisinstance", TypeError) return cls(**marker.kwargs) diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py index b3ef121..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,7 +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) +@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") @@ -76,7 +79,9 @@ async def test_url_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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") @@ -104,7 +109,9 @@ async def test_method_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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") @@ -1101,7 +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) +@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: @@ -1124,7 +1133,9 @@ async def test_multi_value_headers_not_matching_single_value_issued( @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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: @@ -1147,7 +1158,9 @@ async def test_multi_value_headers_not_matching_multi_value_issued( @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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__}"} @@ -1164,7 +1177,9 @@ async def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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={ @@ -1185,7 +1200,9 @@ async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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: @@ -1223,7 +1240,9 @@ async def test_proxy_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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") @@ -1238,7 +1257,9 @@ async def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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") @@ -1317,7 +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) +@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") @@ -1350,7 +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) +@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}) @@ -1365,7 +1390,9 @@ async def test_json_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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}, @@ -1383,7 +1410,9 @@ async def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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}) @@ -1410,7 +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) +@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={ @@ -1431,7 +1462,9 @@ async def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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={ @@ -1452,7 +1485,9 @@ async def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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={ @@ -1486,7 +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) +@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: @@ -1510,7 +1547,9 @@ async def test_headers_not_matching_and_url_and_content_matching( @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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: @@ -1534,7 +1573,9 @@ async def test_url_and_headers_not_matching_and_content_matching( @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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: @@ -1558,7 +1599,9 @@ async def test_url_and_headers_matching_and_content_not_matching( @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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: @@ -1582,7 +1625,9 @@ async def test_headers_matching_and_url_and_content_not_matching( @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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: @@ -1606,7 +1651,9 @@ async def test_url_matching_and_headers_and_content_not_matching( @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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", @@ -1644,7 +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) +@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: @@ -1669,7 +1718,9 @@ async def test_headers_not_matching_and_method_and_url_and_content_matching( @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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: @@ -1694,7 +1745,9 @@ async def test_url_and_headers_not_matching_and_method_and_content_matching( @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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: @@ -1719,7 +1772,9 @@ async def test_method_and_url_and_headers_matching_and_content_not_matching( @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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: @@ -1744,7 +1799,9 @@ async def test_method_and_headers_matching_and_url_and_content_not_matching( @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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: @@ -1769,7 +1826,9 @@ async def test_method_and_url_matching_and_headers_and_content_not_matching( @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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: @@ -1794,7 +1853,9 @@ async def test_method_matching_and_url_and_headers_and_content_not_matching( @pytest.mark.asyncio -@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +@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: diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py index 3348cf4..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,7 +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) +@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") @@ -67,7 +70,9 @@ def test_url_not_matching(httpx_mock: HTTPXMock) -> None: ) -@pytest.mark.httpx_mock(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") @@ -93,7 +98,9 @@ def test_method_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" -@pytest.mark.httpx_mock(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_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get") @@ -134,7 +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) +@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", @@ -872,7 +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) +@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: @@ -894,7 +905,9 @@ def test_multi_value_headers_not_matching_single_value_issued( ) -@pytest.mark.httpx_mock(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: @@ -916,7 +929,9 @@ def test_multi_value_headers_not_matching_multi_value_issued( ) -@pytest.mark.httpx_mock(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__}"} @@ -932,7 +947,9 @@ def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None: ) -@pytest.mark.httpx_mock(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={ @@ -968,7 +985,9 @@ def test_proxy_matching(httpx_mock: HTTPXMock) -> None: assert response.read() == b"" -@pytest.mark.httpx_mock(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_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy") @@ -982,7 +1001,9 @@ def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: ) -@pytest.mark.httpx_mock(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") @@ -1052,7 +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) +@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") @@ -1092,7 +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) +@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}) @@ -1106,7 +1131,9 @@ def test_json_not_matching(httpx_mock: HTTPXMock) -> None: ) -@pytest.mark.httpx_mock(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}, @@ -1123,7 +1150,9 @@ def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: ) -@pytest.mark.httpx_mock(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}) @@ -1148,7 +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) +@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={ @@ -1168,7 +1199,9 @@ def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> Non ) -@pytest.mark.httpx_mock(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={ @@ -1188,7 +1221,9 @@ def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> Non ) -@pytest.mark.httpx_mock(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={ @@ -1220,7 +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) +@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: @@ -1243,7 +1280,9 @@ def test_headers_not_matching_and_url_and_content_matching( ) -@pytest.mark.httpx_mock(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: @@ -1266,7 +1305,9 @@ def test_url_and_headers_not_matching_and_content_matching( ) -@pytest.mark.httpx_mock(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: @@ -1289,7 +1330,9 @@ def test_url_and_headers_matching_and_content_not_matching( ) -@pytest.mark.httpx_mock(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: @@ -1312,7 +1355,9 @@ def test_headers_matching_and_url_and_content_not_matching( ) -@pytest.mark.httpx_mock(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: @@ -1335,7 +1380,9 @@ def test_url_matching_and_headers_and_content_not_matching( ) -@pytest.mark.httpx_mock(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", @@ -1369,7 +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) +@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: @@ -1393,7 +1442,9 @@ def test_headers_not_matching_and_method_and_url_and_content_matching( ) -@pytest.mark.httpx_mock(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: @@ -1417,7 +1468,9 @@ def test_url_and_headers_not_matching_and_method_and_content_matching( ) -@pytest.mark.httpx_mock(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: @@ -1441,7 +1494,9 @@ def test_method_and_url_and_headers_matching_and_content_not_matching( ) -@pytest.mark.httpx_mock(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: @@ -1465,7 +1520,9 @@ def test_method_and_headers_matching_and_url_and_content_not_matching( ) -@pytest.mark.httpx_mock(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: @@ -1489,7 +1546,9 @@ def test_method_and_url_matching_and_headers_and_content_not_matching( ) -@pytest.mark.httpx_mock(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: @@ -1513,7 +1572,9 @@ def test_method_matching_and_url_and_headers_and_content_not_matching( ) -@pytest.mark.httpx_mock(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: diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 791e3a0..2e4880d 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -107,8 +107,8 @@ def unused(*args, **kwargs): def test_httpx_mock_unexpected_request(testdir: Testdir) -> None: """ - Unexpected request should not fail test case if - assert_all_requests_were_expected option is set to False (default). + Unexpected request should fail test case if + assert_all_requests_were_expected option is set to True (default). """ testdir.makepyfile( """ @@ -123,20 +123,26 @@ def test_httpx_mock_unexpected_request(httpx_mock): """ ) result = testdir.runpytest() - result.assert_outcomes(passed=1) + result.assert_outcomes(errors=1, passed=1) + result.stdout.fnmatch_lines( + [ + "*AssertionError: The following requests were not expected:", + "*[]", + ] + ) -def test_httpx_mock_unexpected_request_with_assertion(testdir: Testdir) -> None: +def test_httpx_mock_unexpected_request_without_assertion(testdir: Testdir) -> None: """ - Unexpected request should fail test case if - assert_all_requests_were_expected option is set to True. + 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=True) + @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 @@ -145,13 +151,7 @@ def test_httpx_mock_unexpected_request(httpx_mock): """ ) result = testdir.runpytest() - result.assert_outcomes(errors=1, passed=1) - result.stdout.fnmatch_lines( - [ - "*AssertionError: The following requests were not expected:", - "*[]", - ] - ) + result.assert_outcomes(passed=1) def test_httpx_mock_non_mocked_hosts_sync(testdir: Testdir) -> None: @@ -231,4 +231,4 @@ def test_httpx_mock_non_mocked_hosts_async(httpx_mock): ) result = testdir.runpytest() result.assert_outcomes(errors=1) - result.stdout.re_match_lines([r".*unexpected keyword argument 'foo'"]) + result.stdout.re_match_lines([r".*got an unexpected keyword argument 'foo'"]) From bc1a05d2eec3efdfa696a1ae75adea5d48731785 Mon Sep 17 00:00:00 2001 From: Colin-b Date: Fri, 20 Sep 2024 12:34:18 +0200 Subject: [PATCH 22/23] Keep number of test cases up to date --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a60d1f..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

From ddad01b604d0a0623f099d1ed3af7acf2a7533e1 Mon Sep 17 00:00:00 2001 From: Colin-b Date: Fri, 20 Sep 2024 12:34:31 +0200 Subject: [PATCH 23/23] Release version 0.31.0 today --- CHANGELOG.md | 5 ++++- pytest_httpx/version.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aed2284..841a254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [0.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. @@ -335,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/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"