lundberg / respx

Mock HTTPX with awesome request patterns and response side effects 🦋
https://lundberg.github.io/respx
BSD 3-Clause "New" or "Revised" License
606 stars 40 forks source link

Router does not honor `assert_all_called` #228

Closed BeyondEvil closed 1 year ago

BeyondEvil commented 1 year ago

I'm using the router like so:

import pytest
import httpx
import respx

from syncer.workitem import WorkItem

@pytest.fixture
def router():
    router = respx.Router(base_url="https://dev.azure.com/")
    router.get(
        path__regex=r"\/workitems\/\d+$",
        name="get",
    )
    router.get(
        path__regex=r"\/workitems\/\d+\/comments$",
        name="comments-get",
    )
    router.post(
        path__regex=r"\/workitems\/\d+\/comments$",
        name="comments-post",
    )
    router.patch(
        path__regex=r"\/workitems\/\d+\/comments\/d+$",
        name="comments-patch",
    )
    return router

@pytest.fixture
def http_client(router):
    mock_transport = httpx.MockTransport(router.async_handler)
    return httpx.AsyncClient(transport=mock_transport)

@pytest.fixture
def workitem(http_client):
    def factory():
        return  WorkItem("test_org", http_client)

    return factory

async def test_fetch(router, workitem):
    router["get"].respond(json={})
    await workitem().fetch()
    assert router.routes["get"].call_count == 1
    assert router.routes["comments-get"].call_count == 0
    assert router.routes["comments-post"].call_count == 0
    assert router.routes["comments-patch"].call_count == 0

The test passes, but I would expect it to fail since I'm not calling all mocked routes (which the test itself asserts).

What I'm doing wrong or missunderstanding?

Result of "pip freeze"

anyio==3.6.2 assertpy==1.1 async-generator==1.10 attrs==23.1.0 azure-core==1.26.4 azure-servicebus==7.9.0 black==23.3.0 certifi==2022.12.7 charset-normalizer==3.1.0 click==8.1.3 exceptiongroup==1.1.1 flake8==6.0.0 flake8-bugbear==23.3.23 Flake8-pyproject==1.2.3 h11==0.14.0 httpcore==0.17.0 httpx==0.24.0 idna==3.4 iniconfig==2.0.0 isodate==0.6.1 mccabe==0.7.0 mypy-extensions==1.0.0 outcome==1.2.0 packaging==23.1 pathspec==0.11.1 pep8-naming==0.13.3 platformdirs==3.5.0 pluggy==1.0.0 pycodestyle==2.10.0 pyflakes==3.0.1 pytest==7.3.1 pytest-mock==3.10.0 pytest-trio==0.8.0 requests==2.30.0 respx==0.20.1 six==1.16.0 sniffio==1.3.0 sortedcontainers==2.4.0 -e git+https://afaforsakring2@dev.azure.com/afaforsakring2/Utvecklingsstod/_git/syncer@611d0bcb65a906f82c0927725347140eab791774#egg=syncer tomli==2.0.1 trio==0.22.0 typing_extensions==4.5.0 uamqp==1.6.4 urllib3==2.0.2

FWIW I'm using python 3.10.6 running in WSL2 (Ubuntu 22.04) on Windows 10.

lundberg commented 1 year ago

Thanks for reporting @BeyondEvil.

You're right, assert_all_called is not evaluated when using the base Router .. only when using the MockRouter, e.g. with respx.mock():.

This is due to missing exit stack when using the httpx client like this. Meaning, assert_all_called needs to be evaluated after the/each test function, which it does when using the MockRouter either as a decorator, contextmanager or fixture with ctx+yield.

Currently I'd say you'll have to trigger it manually. Thinking out loud here, but could you try this ...

@pytest.fixture
def router():
    router = respx.Router(base_url="https://dev.azure.com/")
    # ...
    yield router
    router.assert_all_called()

Regardless if this manual approach works, we should adjust the documentation mentioning this current limitation.

Maybe even remove the assert_all_called init kwarg from Router and only have it on the MockRouter making it clear that it doesn't gets triggered automatically by the base router.

BeyondEvil commented 1 year ago

Thanks for reporting @BeyondEvil.

You're right, assert_all_called is not evaluated when using the base Router .. only when using the MockRouter, e.g. with respx.mock():.

This is due to missing exit stack when using the httpx client like this. Meaning, assert_all_called needs to be evaluated after the/each test function, which it does when using the MockRouter either as a decorator, contextmanager or fixture with ctx+yield.

Thanks for detailed explanation!

Currently I'd say you'll have to trigger it manually. Thinking out loud here, but could you try this ...

@pytest.fixture
def router():
    router = respx.Router(base_url="https://dev.azure.com/")
    # ...
    yield router
    router.assert_all_called()

Brilliant!

Regardless if this manual approach works, we should adjust the documentation mentioning this current limitation.

Maybe even remove the assert_all_called init kwarg from Router and only have it on the MockRouter making it clear that it doesn't gets triggered automatically by the base router.

Yes and yes. 👍 😊

lundberg commented 1 year ago

Added a new issue #233 for this 👍

lundberg commented 1 year ago

Docs are now updated @BeyondEvil

BeyondEvil commented 1 year ago

Nice! Clear and concise. 👍