Async tests and fixtures
Pytest already has the official
pytest-asyncio plugin which allows
you to write async tests and fixtures.
pytest-asyncio docs, you can have an
async fixture like this:
from typing import Any, AsyncGenerator import pytest import pytest_asyncio from httpx import AsyncClient @pytest_asyncio.fixture async def client() -> AsyncGenerator[AsyncClient, Any]: async with AsyncClient() as c: yield c @pytest.mark.asyncio async def test_api_call(client: AsyncClient) -> None: response = await client.get("http://test.com") assert response.status_code == 200
There are a few points to consider here:
- Obviously the fixture and test are defined as
async defsince that was the intention.
- The fixture should be decorated with
@pytest_asyncio.fixtureso pytest can pick this up properly.
- The tests should be marked with
As an alternative you can define a variable
pytestmark = pytest.mark.asyncio
in your test file to treat all tests as async and avoid the repetition.
This works well but with some tweaks it can be made simpler.
pytest-asyncio offers a
discovery mode concept which allows you to control how
async tests are discovered.
If your project only uses
asyncio as the asynchronous programming library,
you can take advantage of the discovery mode to make this simpler:
from typing import Any, AsyncGenerator import pytest from httpx import AsyncClient @pytest.fixture async def client() -> AsyncGenerator[AsyncClient, Any]: async with AsyncClient() as c: yield c async def test_api_call(client: AsyncClient) -> None: response = await client.get("http://test.com") assert response.status_code == 200
- You can define fixtures with normal
- You don’t need to mark tests as async, so no decorators needed.
You can now run the tests with:
$ pytest tests --asyncio-mode=auto
If you are using
pyproject.toml file you can add this with:
[tool.pytest.ini_options] asyncio_mode = "auto"
And run the tests with:
$ pytest tests
Asyncio event loop
When testing a
you might get the error
<Task pending> attached to a different loop.
The reason is that you should define event loop for pytest to use.
You can add the
event_loop fixture to
import asyncio from typing import Any, Generator import pytest @pytest.fixture(scope="session") def event_loop() -> Generator[asyncio.AbstractEventLoop, Any, None]: loop = asyncio.get_event_loop() yield loop loop.close()