pytest_asyncio
pytest-asyncio is a third-party plugin for
pytest that adds support for testing asynchronous code — functions
written with async def and the
asyncio library.
Plain pytest and
unittest do not run
async def tests natively without extra wiring.
Testing with pytest_asyncio
# code under test
import asyncio
async def fetch_data():
await asyncio.sleep(0.1)
return "Hello, Async!"
# test code — requires: pip install pytest pytest-asyncio
import pytest
@pytest.mark.asyncio
async def test_fetch_data():
result = await fetch_data() # directly await inside the test
assert result == "Hello, Async!"
Run: pytest test_fetch.py
unittest vs pytest vs pytest_asyncio
| unittest (PyUnit) | pytest | pytest + pytest_asyncio | |
|---|---|---|---|
| What it is | Built-in Python test framework (import unittest) |
Third-party test runner and framework (pip install pytest)
|
pytest plugin for async tests (pip install pytest-asyncio)
|
| Test style |
Classes inheriting unittest.TestCase; methods named
test_*
|
Plain functions or classes; test_* functions; minimal
boilerplate
|
Same as pytest — async def test_* with
@pytest.mark.asyncio
|
| Assertions |
self.assertEqual, self.assertTrue, …
|
Plain assert — failures show rich diffs |
Plain assert inside async def tests |
| Fixtures |
setUp / tearDown, setUpClass
|
@pytest.fixture — composable, scoped
(function/module/session)
|
@pytest_asyncio.fixture for async setup/teardown |
| Sync code testing | Yes — primary use case | Yes — primary use case | Yes — plugin does not replace sync tests; both coexist |
| Async code testing |
No native async def tests. Wrap with
asyncio.run(coro) inside a sync
test_* method
|
Does not run async def tests by
default — they are skipped or error without a plugin
|
Yes — plugin runs coroutine tests on an event loop |
| Async I/O (network, DB, files) |
Call async API via asyncio.run() in sync test; awkward
for multiple awaits
|
Same workaround without plugin; no built-in event loop |
Natural await on httpx, aiohttp, async DB drivers,
FastAPI
AsyncClient
|
| Discovery / run |
python -m unittest or unittest.main()
|
pytest — auto-discovers tests, plugins, markers |
pytest with plugin loaded (auto if installed) |
| Dependencies | None (stdlib) | pytest |
pytest + pytest-asyncio |
| Best for | Simple projects, Django defaults, teams wanting stdlib only | Most Python projects; sync unit and integration tests |
FastAPI, aiohttp, async libraries, any
async/await codebase
|
Testing async code with pytest
If you write async def test_foo() and run plain pytest
without pytest-asyncio
Pytest collects the function but does not await it —
the coroutine is never executed, so the test may pass everytime.
# Plain pytest — sync wrapper (no pytest-asyncio)
import asyncio
async def fetch_data():
return "Hello, Async!"
def test_fetch_data_sync_wrapper():
result = asyncio.run(fetch_data())
assert result == "Hello, Async!"
With pytest-asyncio: the plugin detects
@pytest.mark.asyncio (or auto mode), creates/reuses an
event loop, runs the coroutine to completion, and reports failures like
any other pytest test.
Drawbacks of pytest + pytest_asyncio for async code
| Drawback | Detail |
|---|---|
| Extra dependency |
Not in stdlib — CI and dev env must install
pytest-asyncio
|
| Event loop scope |
Loop per function vs module vs session (asyncio_mode,
fixture scope) — wrong choice causes "loop is closed" or leaked
resources
|
| Plugin configuration |
pytest.ini: asyncio_mode = auto or
explicit markers — team must agree on one mode
|
| Mixing sync and async |
Calling blocking I/O inside async def tests blocks the
loop — use asyncio.to_thread() or async libraries
|
| Debugging | Async stack traces and "Task was destroyed but pending" harder to read than sync tests |
| Not a replacement for unittest/pytest | Plugin only adds async execution — you still choose pytest over unittest for the base runner and fixtures |
Where async I/O fits in pytest
Async I/O means non-blocking operations — HTTP calls, WebSockets, async
database drivers (asyncpg, motor), file I/O
(aiofiles). In pytest with pytest-asyncio:
| Layer | Role in pytest |
|---|---|
pytest |
Discovers tests, runs fixtures, reports pass/fail |
pytest-asyncio |
Provides event loop; runs async def tests and async
fixtures
|
@pytest.mark.asyncio |
Marks a test coroutine for the plugin to execute |
@pytest_asyncio.fixture |
Async setup — e.g. open AsyncClient, DB pool, then
yield to test
|
| Async libraries under test |
httpx.AsyncClient, FastAPI test client,
aiohttp
— awaited directly inside the test body
|
# Async I/O example — FastAPI + httpx
import pytest
from httpx import AsyncClient, ASGITransport
from myapp import app
@pytest.mark.asyncio
async def test_read_root():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/")
assert response.status_code == 200
Summary: use unittest for stdlib sync
tests; use pytest for ergonomic sync tests; add
pytest-asyncio when the code under test uses
async/await and you want to await directly in
tests instead of wrapping everything in asyncio.run().