3

I need to write test case for a function which use to fetch data from API. In there i used httpx.AsyncClient() as context manager. But i don't understand how to write test case for that function.

async def make_dropbox_request(url, payload, dropbox_token):
async with httpx.AsyncClient(timeout=None, follow_redirects=True) as client:
    headers = {
        'Content-Type': 'application/json',
        'authorization': 'Bearer '+ dropbox_token
    }
    # make the api call
    response = await client.post(url, headers=headers, json=payload)
    
    if response.status_code not in [200]:
        print('Dropbox Status Code: ' + str(response.status_code))

    if response.status_code in [200, 202, 303]:
        return json.loads(response.text)

    elif response.status_code == 401:
        raise DropboxAuthenticationError()

    elif response.status_code == 429:
        sleep_time = int(response.headers['Retry-After'])
        if sleep_time < 1*60:
            await asyncio.sleep(sleep_time)
            raise DropboxMaxRateLimitError()
        raise DropboxMaxDailyRateLimitError()

    raise DropboxHTTPError()

I need to write test cases without calling the API. So there for i believe in this case i need to mock client.post() but i do not understand how to do that. If anyone can help me to figure this out that would be really helpful for me.

This image also include my code block

4 Answers4

3

I also faced with same issue and handled it with patch decorator. I share my code, so that might help for others.

from unittest.mock import patch
import pytest
import httpx
from app.services import your_service


@pytest.mark.anyio
@patch(
    'app.services.your_service.httpx.AsyncClient.post',
    return_value = httpx.Response(200, json={'id': '9ed7dasdasd-08ff-4ae1-8952-37e3a323eb08'})
)
async def test_get_id(mocker):        
    result = await your_service.get_id()
    assert result == '9ed7dasdasd-08ff-4ae1-8952-37e3a323eb08'
Ismayil Ibrahimov
  • 440
  • 1
  • 7
  • 11
2

TL;DR: use return_value.__aenter__.return_value to mock the async context.

Assuming you are using Pytest and pytest-mock, your can use the mocker fixture to mock httpx.AsyncClient.

Since the post function is async, you will need to use an AsyncMock. Finally, since you use an async context, you will also need to use return_value.__aenter__.return_value to properly mock the returned context. Note for a synchronous context, simply use __enter__ instead of __aenter__.

@pytest.fixture
def mock_AsyncClient(mocker: MockerFixture) -> Mock:
      mocked_AsyncClient = mocker.patch(f"{TESTED_MODULE}.AsyncClient")

      mocked_async_client = Mock()
      response = Response(status_code=200)
      mocked_async_client.post = AsyncMock(return_value=response)
      mocked_AsyncClient.return_value.__aenter__.return_value = mocked_async_client

      return mocked_async_client
smichaud
  • 373
  • 3
  • 9
1

You can try out the RESPX mocking library to test and mock your HTTPX clients. ​ ​ In your case, something like this should do it: ​ ​

async def make_dropbox_request(url, payload, dropbox_token):
    ...
    response = await client.post(url, headers=headers, json=payload)
    ...
    return response.json()
​
​
@respx.mock
async def test_dropbox_endpoint():
    url = "https://dropbox-api/some-endpoint/"
    endpoint = respx.post(url).respond(json={"some": "data"})
    result = await make_dropbox_request(url, ..., ...)
    assert endpoint.called
    assert result == {"some": "data"}

​ To be dry and not repeat the mocking in each test, you can set up your own pytest fixture, or respx instance, globally that pre-mocks all dropbox api endpoints, and then in each test just alter response/error depending on the scenario for the test, to get full test coverage on make_dropbox_request. ​

@pytest.fixture()
async def dropbox_mock():
    async with respx.mock() as dropbox:
        # default endpoints and their responses
        dropbox.post("some-endpoint", name="foo").respond(404)
        dropbox.post("some-other-endpoint", name="bar").respond(404)
        #                                     ^ name routes for access in tests
        yield dropbox
​
​
async def test_some_case(dropbox_mock):
    dropbox_mock["foo"].respond(json={})
    ....
Jocke
  • 673
  • 3
  • 8
0

If you are using pytest, you can rely on pytest-httpx to mock the Client and/or the AsyncClient from httpx and prevent any request to actually be issued. Note that, as stated in the documentation, you can still add some exceptions for dedicated hostnames, should you need this.

As you did not specify what you actually want to test, I will let you figure out what you want to check (url, headers, body, ...), but the following sample will ensure the requests are not really reaching the related hosts:

import pytest

@pytest.mark.asyncio
async def test_something_async(httpx_mock):
    await make_dropbox_request(...)

Note that you do not need to use an async test case to mock your AsyncClient (in case your actual async call can be triggered in a sync way).

Colin B
  • 94
  • 6