sanic-testing icon indicating copy to clipboard operation
sanic-testing copied to clipboard

Cannot patch the 'httpx' requests while unit testing

Open srbssv opened this issue 2 years ago • 1 comments

Sanic-testing uses httpx under the hood, which makes monkeypatching httpx requests hard to implement.

For example if we have and endpoint which makes external requests, and if we monkeypatch this request, then it patches an actual request to endpoint:

# views.py
import httpx
...
@app.get('/test')
async def view_test(request):
    async with httpx.AsyncClient() as client:
        api_response = await client.get(
            'https://jsonplaceholder.typicode.com/todos/1',
            timeout=10,
        )
        resp = api_response.json()
        resp['foo'] = 0
        return HTTPResponse(json.dumps(resp), 200)

Test function:

# test_views.py
import httpx, pytest
...
# The `client` parameter is the fixture of web app
def test_view_test(client, monkeypatch):
    async def return_mock_response(*args, **kwargs):
        return httpx.Response(200, content=b'{"response": "response"}')

    monkeypatch.setattr(httpx.AsyncClient, 'get', return_mock_response)
    _, response = client.test_client.get('/test')
    assert response.json == {'response': 'response', 'foo': 0}
    assert response.status_code == 200

Here, we replace our request _, response = client.test_client.get('/test') with the patched one. Which makes us customize the patch by excluding the host address.

srbssv avatar Dec 28 '22 08:12 srbssv

This might be as simple as exposing a fixture like this:

@pytest.fixture
def httpx():
    orig = AsyncClient.request
    mock = AsyncMock()

    async def request(self, method, url, **kwargs):
        if "127.0.0.1" in url:
            return await orig(self, method, url, **kwargs)

        return await mock(method, url, **kwargs)

    AsyncClient.request = request
    yield mock
    AsyncClient.request = orig

Then ...

def test_outgoing(app: Sanic, httpx: AsyncMock):
    httpx.return_value = Response(201, json={"foo": "bar"})

    _, response = app.test_client.post("")
    assert response.status == 201
    assert response.json == {"foo": "bar"}
    httpx.assert_awaited_once_with(
        "POST",
        "https://httpbin.org/post",
        content=ANY,
        files=ANY,
        data=ANY,
        json=ANY,
        cookies=ANY,
        params=ANY,
        headers=ANY,
        auth=ANY,
        follow_redirects=ANY,
        timeout=ANY,
        extensions=ANY,
    )

ahopkins avatar Dec 28 '22 08:12 ahopkins