Async tests and fixtures
Pytest already has the official pytest-asyncio plugin which allows
you to write async tests and fixtures.
According to pytest-asyncio docs, you can have an async test
with 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
@pytest.mark.asynciodecorator.
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.
Discovery mode
This works well but with some tweaks it can be made simpler.
The 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
This way:
- You can define fixtures with normal
@pytest.fixturedecorator. - 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 FastAPI or SQLAlchemy project,
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 conftest.py with:
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()