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().