pytest-asyncio icon indicating copy to clipboard operation
pytest-asyncio copied to clipboard

Async test function gets skipped with `asyncio_mode = auto` and `@unittest.mock.patch.dict` decorator

Open nphilipp opened this issue 2 years ago • 8 comments

Hi!

In a PR for a project I'm working on, an async test method gets skipped with a PytestUnhandledCoroutineWarning even though pytest-asyncio is installed and asyncio_mode is auto. I poked at it for while and found that this is because the method was decorated with @unittest.mock.patch.dict(...). Here's a minimal reproducer which exposes the issue with Python 3.10.6, pytest-7.1.2, pytest-asyncio-0.19.0:

--- 8< --- test_the_things.py ---

from unittest import mock

import pytest

bar = {}


async def test_runs():
    pass


@mock.patch.dict("test_the_things.bar")
async def test_doesnt_run():
    pass


@pytest.mark.asyncio
@mock.patch.dict("test_the_things.bar")
async def test_runs_anyway():
    pass

--- >8 ---

Here's the output I get:

(pytest-asyncio-issue) nils@makake:~/test/python/pytest-asyncio> pytest --asyncio-mode=auto -v
======================================== test session starts =========================================
platform linux -- Python 3.10.6, pytest-7.1.2, pluggy-1.0.0 -- /home/nils/.virtualenvs/pytest-asyncio-issue/bin/python
cachedir: .pytest_cache
rootdir: /home/nils/test/python/pytest-asyncio
plugins: asyncio-0.19.0
asyncio: mode=auto
collected 3 items                                                                                    

test_the_things.py::test_runs PASSED                                                           [ 33%]
test_the_things.py::test_doesnt_run SKIPPED (async def function and no async plugin instal...) [ 66%]
test_the_things.py::test_runs_anyway PASSED                                                    [100%]

========================================== warnings summary ==========================================
test_the_things.py::test_doesnt_run
  /home/nils/.virtualenvs/pytest-asyncio-issue/lib/python3.10/site-packages/_pytest/python.py:181: PytestUnhandledCoroutineWarning: async def functions are not natively supported and have been skipped.
  You need to install a suitable plugin for your async framework, for example:
    - anyio
    - pytest-asyncio
    - pytest-tornasync
    - pytest-trio
    - pytest-twisted
    warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid)))

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
============================== 2 passed, 1 skipped, 1 warning in 0.01s ===============================
sys:1: RuntimeWarning: coroutine 'test_doesnt_run' was never awaited
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
(pytest-asyncio-issue) nils@makake:~/test/python/pytest-asyncio>

Either without the decorator or by explicitly adding @pytest.mark.asyncio the async test functions don't get skipped.

nphilipp avatar Aug 29 '22 16:08 nphilipp

This also happens when other marks are applied.

@pytest.mark.integration
async def test_runs():
    ...

The test gets skipped with the same warning listed above when running pytest -m integraton

As above applying the decorator fixes it:

@pytest.mark.integration
@pytest.mark.asyncio
async def test_runs():
    ...

sanders41 avatar Sep 01 '22 18:09 sanders41

On closer inspection after looking at this issue I see our tests that are skipped are also marked with a patch @mock.patch("patched.function"). We found a few marked as @pytest.mark.integration that didn't get skipped so it seems as though it is specific to patching.

sanders41 avatar Sep 01 '22 18:09 sanders41

When I did a test a couple of days ago I could reproduce the issue with @mock.patch.dict, but not @mock.patch. I found that patch performs some special treatment for coroutines, which patch.dict does not.

In my opinion there are two things to be done here:

  1. Investigate if this is a CPython bug
  2. Find an intermediate solution for pytest-asyncio

seifertm avatar Sep 06 '22 16:09 seifertm

In our code base it has been hard for me to track down the exact cause because it has shown up in some places I didn't expect and not in some I did. I know we do have @mock.patch.dict in places so that could be what is happening.

I looked into it some with @mock.patch.dict and saw the function no longer gets reported as an async function with inspect which seems to match up with what you found.

sanders41 avatar Sep 06 '22 17:09 sanders41

I opened a CPython issue regarding this: https://github.com/python/cpython/issues/98086

seifertm avatar Oct 08 '22 06:10 seifertm

CI

stillborn333 avatar Oct 09 '22 12:10 stillborn333

The issue has been resolved for CPython 3.12 and the patch was backported to CPython 3.10 and CPython 3.11.

3.10.9 is scheduled for 2022-12-05 and 3.11.1 should be released in December as well.

CPython 3.7 is not affected by this bug, but the question is if we want to include a workaround for 3.8 and 3.9.

seifertm avatar Dec 01 '22 18:12 seifertm

The issue continues to be valid. If anyone wants to resolve it for Python 3.8 or 3.9, they're welcome to provide a backport that can be included in pytest-asyncio.

seifertm avatar Jun 27 '23 14:06 seifertm