pytest-asyncio
The pytest plugin for async tests. Without it, async def test_foo() is collected as a coroutine and never awaited — silent pass.
Install + minimal use
pip install pytest-asyncio
import pytest
@pytest.mark.asyncio
async def test_async_thing():
result = await fetch_data()
assert result == "ok"
The marker tells pytest to run the coroutine inside an event loop.
Modes: auto vs strict
pytest-asyncio has two modes, set in pyproject.toml or pytest.ini:
[tool.pytest.ini_options]
asyncio_mode = "auto" # treat all async def tests as asyncio
# or
asyncio_mode = "strict" # require explicit @pytest.mark.asyncio (default)
strict(default in v0.21+): every async test needs the marker. Explicit, less magic.auto: anyasync def test_*is auto-marked. Less typing, but masks if a test isn’t actually async.
For new projects, auto is fine and removes boilerplate. For mixed sync/async codebases, strict makes intent obvious.
Async fixtures
@pytest_asyncio.fixture
async def client():
async with httpx.AsyncClient() as c:
yield c
@pytest.mark.asyncio
async def test_get(client):
r = await client.get("https://example.com")
assert r.status_code == 200
In auto mode, @pytest.fixture works for async fixtures too. In strict mode, use @pytest_asyncio.fixture to be explicit.
Event loop scope
By default, every test gets a fresh event loop. Cross-test state (connection pools, asyncio Tasks) doesn’t bleed.
To share a loop across a module/session (needed for session-scoped async fixtures):
# conftest.py
import pytest
@pytest.fixture(scope="session")
def event_loop_policy():
return asyncio.DefaultEventLoopPolicy()
Or in v0.23+:
[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function" # or session, module, class
A common error chain:
- Session-scoped async fixture creates a Connection.
- Each test creates a new event loop.
- The Connection is bound to the first loop; subsequent tests fail with “loop is closed” or “got Future attached to a different loop.”
Fix: scope the loop the same as the fixture, or make the fixture function-scoped.
Mocking async functions
from unittest.mock import AsyncMock
@pytest.mark.asyncio
async def test_with_mock():
mock_fetch = AsyncMock(return_value={"ok": True})
result = await mock_fetch()
assert result == {"ok": True}
mock_fetch.assert_awaited_once()
AsyncMock returns coroutines. Use assert_awaited_once() (not assert_called_once()) for awaitables.
In MagicMock(spec=SomeAsyncClass), methods declared async are automatically AsyncMock instances (Python 3.8+).
@patch("myapp.api.fetch_data", new_callable=AsyncMock)
async def test_call(mock_fetch):
mock_fetch.return_value = {"ok": True}
...
See 05_mock_vs_magicmock_patch.md.
Testing async generators
async def stream():
for i in range(3):
yield i
@pytest.mark.asyncio
async def test_stream():
items = [x async for x in stream()]
assert items == [0, 1, 2]
Or collect manually:
gen = stream()
assert await gen.__anext__() == 0
Timeout protection
Long-hanging async tests are awful. Use pytest-timeout or asyncio.wait_for:
@pytest.mark.asyncio
async def test_with_timeout():
async with asyncio.timeout(2.0): # 3.11+
await slow_thing()
Or globally:
[tool.pytest.ini_options]
timeout = 30 # via pytest-timeout
Sync + async tests in one project
Both work side by side. Tests not marked async run synchronously; fixtures detect their own type.
def test_sync():
assert 1 + 1 == 2
@pytest.mark.asyncio
async def test_async():
await asyncio.sleep(0)
Common pitfalls
- Forgot the marker —
async def test_x()runs but doesn’tawaitanything. The test “passes” because the coroutine is created but never executed. Symptoms: warnings about unawaited coroutines. - Loop closed errors — fixture loop scope mismatch. See above.
- Mixing
asyncioandanyio/trio—pytest-asynciois asyncio-specific. For trio/anyio, usepytest-anyio. AsyncMockconfused withMagicMock— calling aMagicMock()returns a Mock, not a coroutine.awaitit and you getTypeError: object MagicMock can't be used in 'await' expression.- Tasks leaking across tests — fire-and-forget
asyncio.create_task(...)whose lifetime exceeds the test. Track them; cancel in teardown.
Interview angle
- Q: “How do you test an async function in pytest?” —
pytest-asyncioplugin, mark with@pytest.mark.asyncioorautomode. - Q: “Difference between auto and strict modes?” — auto = no marker needed; strict = explicit.
- Follow-up: “What’s
AsyncMockand when do you need it?” — for mocking async functions; returns awaitable. - Follow-up: “Why does the test fail with ‘loop is closed’?” — session-scoped fixture bound to the wrong loop; align scopes.
See 04_async_concurrency/05_async_python_coroutines.md for asyncio fundamentals.