diff --git a/doc/howto.rst b/doc/howto.rst index d726ca3..04a5609 100644 --- a/doc/howto.rst +++ b/doc/howto.rst @@ -237,6 +237,24 @@ these. .. literalinclude :: ../tests/examples/test_howto_check.py :language: python +To check automatically when a block finishes, use +:py:meth:`pytest_httpserver.HTTPServer.with_check`: + +.. code-block:: python + + httpserver.expect_ordered_request("/first").respond_with_data(status=204) + httpserver.expect_ordered_request("/second").respond_with_data(status=204) + with httpserver.with_check(): + requests.get(httpserver.url_for("/first")) + requests.get(httpserver.url_for("/second")) + +This checks unexpected requests even after all ordered handlers have been used. +It also propagates errors raised by response handlers. Checks run only when the +block exits normally; an exception raised in the block propagates unchanged. +The context manager does not wait for background clients, so complete their +work before leaving the block. Use ``wait()`` when you need to wait for handler +completion. + .. note:: The scope of the errors checked by the ``check()`` method may change in the future - it is added to check all possible errors happened in diff --git a/pytest_httpserver/httpserver.py b/pytest_httpserver/httpserver.py index 2919164..fdb3a3f 100644 --- a/pytest_httpserver/httpserver.py +++ b/pytest_httpserver/httpserver.py @@ -839,6 +839,20 @@ def check(self) -> None: self.check_assertions() self.check_handler_errors() + @contextmanager + def with_check(self) -> Generator[Self, None, None]: + """ + Check server-side assertions and handler errors when the block exits normally. + + Calls :py:meth:`check` after the body completes. Exceptions raised in the body + propagate without running the checks, so a server-side error cannot replace them. + This context manager does not wait for requests or stop the server. + + :return: the server instance + """ + yield self + self.check() + def check_assertions(self) -> None: """ Raise AssertionError when at least one assertion added diff --git a/releasenotes/notes/with-check-83d6f1a024ca738b.yaml b/releasenotes/notes/with-check-83d6f1a024ca738b.yaml new file mode 100644 index 0000000..3b39c92 --- /dev/null +++ b/releasenotes/notes/with-check-83d6f1a024ca738b.yaml @@ -0,0 +1,7 @@ +features: + - | + Add ``HTTPServer.with_check()``, a context manager that checks server-side + assertions and handler errors when its body completes normally. It detects + unexpected requests received after the expected ordered requests without + changing ``wait()`` semantics. Exceptions raised in the body propagate + unchanged. diff --git a/tests/test_handler_errors.py b/tests/test_handler_errors.py index 288dcd3..869af14 100644 --- a/tests/test_handler_errors.py +++ b/tests/test_handler_errors.py @@ -90,3 +90,47 @@ def handler3(_): with pytest.raises(ValueError): # noqa: PT011 httpserver.check() + + +def test_with_check_success(httpserver: HTTPServer): + httpserver.expect_ordered_request("/first").respond_with_data(status=204) + httpserver.expect_ordered_request("/second").respond_with_data(status=204) + + with httpserver.with_check() as server: + assert server is httpserver + assert requests.get(server.url_for("/first")).status_code == 204 + assert requests.get(server.url_for("/second")).status_code == 204 + + +def test_with_check_unexpected_request_after_expected_requests(httpserver: HTTPServer): + httpserver.expect_ordered_request("/first").respond_with_data(status=204) + httpserver.expect_ordered_request("/second").respond_with_data(status=204) + + with pytest.raises(AssertionError, match="No handler found for request"), httpserver.with_check(): + assert requests.get(httpserver.url_for("/first")).status_code == 204 + assert requests.get(httpserver.url_for("/second")).status_code == 204 + assert requests.get(httpserver.url_for("/unexpected")).status_code == 500 + + +def test_with_check_propagates_handler_error(httpserver: HTTPServer): + handler_error = ValueError("handler failed") + + def handler(_) -> werkzeug.Response: + raise handler_error + + httpserver.expect_request("/error").respond_with_handler(handler) + + with pytest.raises(ValueError, match="handler failed") as error, httpserver.with_check(): + assert requests.get(httpserver.url_for("/error")).status_code == 500 + + assert error.value is handler_error + + +def test_with_check_preserves_body_exception(httpserver: HTTPServer): + body_error = RuntimeError("client failed") + + with pytest.raises(RuntimeError, match="client failed") as error, httpserver.with_check(): + assert requests.get(httpserver.url_for("/unexpected")).status_code == 500 + raise body_error + + assert error.value is body_error