简体   繁体   中英

Call count not working with async function

I have a function that has a decorator @retry , which retries the function if a certain Exception happened. I want to test that this function executes the correct amount of times, for which I have the following code which is working:

@pytest.mark.asyncio
async def test_redis_failling(mocker):
    sleep_mock = mocker.patch.object(retry, '_sleep')
    with pytest.raises(ConnectionError):
        retry_store_redis()
    assert sleep_mock.call_count == 4

@retry(ConnectionError, initial_wait=2.0, attempts=5)
def retry_store_redis():
    raise ConnectionError()

But, if I modify retry_store_redis() to be an async function, the return value of sleep_mock.call_count is 0.

So you define "retry" as a function. Then you define a test, then you define some code that uses @retry.

@retry, as a decorator, is being called at import time. So the order of operations is

  1. declare retry
  2. declare test
  3. call retry with retry_store_redis as an argument
  4. start your test
  5. patch out retry
  6. call the function you defined in step 3

so "retry" gets called once (at import time), your mock gets called zero times. To get the behavior you want, (ensuring that retry is actually re-calling the underlying function) I would do

@pytest.mark.asyncio
async def test_redis_failling(mocker):
    fake_function = MagicMock(side_effect=ConnectionError)

    decorated_function = retry(ConnectionError, initial_wait=2.0, attempts=5)(fake_function)

    with pytest.raises(ConnectionError):
        decorated_function()
    assert fake_function.call_count == 4

if you wanted to test this as built (instead of a test specifically for the decorator) you would have to mock out the original function inside the decorated function- which would depend on how you implemented the decorator. The default way (without any libraries) means you would have to inspect the "closure" attribute. You can build the object to retain a reference to the original function though, here is an example

def wrap(func):
    class Wrapper:
        def __init__(self, func):
            self.func = func

        def __call__(self, *args, **kwargs):
            return self.func(*args, **kwargs)
    return Wrapper(func)


@wrap
def wrapped_func():
  return 42

in this scenario, you could patch the wrapped function at wrapped_func.func

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM