pytest-dev / pytest-asyncio

Asyncio support for pytest
https://pytest-asyncio.readthedocs.io
Apache License 2.0
1.36k stars 140 forks source link

pytest-asyncio unusable with code using `aiohttp` #868

Open MarkusSintonen opened 3 days ago

MarkusSintonen commented 3 days ago

It seems pytest-asyncio (0.23.7) is currently badly broken with code relying on aiohttp.

This simple example currently breaks. The example is heavily simplified from the actual fixture setup. This used to work fine before 0.22.

import aiohttp
import pytest

# No longer working for getting a single event loop for aiohttp
# @pytest.fixture(scope="session")
# def event_loop():
#    loop = asyncio.get_event_loop_policy().new_event_loop()
#    try:
#        yield loop
#    finally:
#        loop.close()

@pytest.fixture(scope="session")
async def client(
    # event_loop <- accessing this no longer working. But neither this works without it
):
    async with aiohttp.ClientSession(
        # loop=event_loop <- aiohttp requires using a single event_loop that can not be closed between the tests...
    ) as session:
        yield session

async def test_the_client(client: aiohttp.ClientSession):
    await client.get("http://localhost:8080/foobar")  # RuntimeError: Timeout context manager should be used inside a task

Using asyncio_mode = "auto":

[tool.poetry]
name = "foobar"
package-mode = false

[tool.poetry.dependencies]
python = "~3.12"
aiohttp = "==3.9.5"

[tool.poetry.dev-dependencies]
pytest = "==8.2.2"
pytest-asyncio = "==0.23.7"

[tool.pytest.ini_options]
asyncio_mode = "auto"
MarkusSintonen commented 3 days ago

Using this https://pytest-asyncio.readthedocs.io/en/latest/how-to-guides/run_session_tests_in_same_loop.html doesnt have any effect. Seems there are multiple issues now in pytest-asyncio. There is no longer access to a single event loop used for tests. Also pytest-asyncio is now closing the event loop between the tests. How do I get back the previous working behaviour which is compatible eg with aiohttp? It is neither possible to change the session scoped client fixture to a function fixture. (Also it doesnt make sense, we want a single client per test session)

MarkusSintonen commented 3 days ago

There seems to be multiple open issues related to latest version of pytest-asyncio. Couldn't find anything directly related to aiohttp usage.

seifertm commented 1 day ago

Thanks for the code example.

In your specific code, the issue is that the test and the fixture run in different asyncio event loops. Each pytest scope has its own event loop. When specifying a @pytest.fixture(scope="session"), the fixture code runs in a session-scoped loop, whereas test_the_client uses the default function-scoped loop.

Adding @pytest.mark.asyncio(scope="session") to test_the_client make the test code to run in the same session-scoped loop, thus eliminating the issue.

However, there's a tightly linked know issue in pytest-asyncio, which makes it impossible to separate the scope of the event loop from the scope of the pytest fixture. Sometimes, it's necessary to have code that runs in a session-wide event loop, but that code should be re-executed for every function. This is not possible with v0.23 at the moment. If you require this functionality, you should pin pytest-asyncio to v0.21 until the next major release. This issue is tracked in #706.

As for why the code mentioned in the docs doesn't work, that's an entirely different topic that needs investigating.

seifertm commented 1 day ago

When I add the following code snippet to conftest.py (instead of @pytest.mark.asyncio(scope="session" as mentioned in my previous comment), your provided example runs as expected.

import pytest

from pytest_asyncio import is_async_test

def pytest_collection_modifyitems(items):
    pytest_asyncio_tests = (item for item in items if is_async_test(item))
    session_scope_marker = pytest.mark.asyncio(scope="session")
    for async_test in pytest_asyncio_tests:
        async_test.add_marker(session_scope_marker, append=False)

I referred to the exact same documentation page as you did (see How to run all tests in the session in the same event loop).

Do you think you could provide another code example where the code snippet in the docs doesn't solve the issues you're experiencing?