How to Test Async Python Code with pytest-asyncio
tags: python, testing, async, tutorial
How to Test Async Python Code with pytest-asyncio
You’ve written elegant async code that handles database queries, API calls, and background tasks with async def. But when you try to test it with plain pytest, your tests silently pass, skip, or hang forever. That’s because pytest doesn’t natively support async functions—it treats async def test_... as a regular function that returns a coroutine object, which it never actually runs. The result? A false sense of security while your async logic remains untested.
Enter pytest-asyncio, the plugin that bridges this gap. It teaches pytest to drive an event loop, await your test coroutines, and manage async fixtures properly. In this guide, you’ll learn how to set it up, write real async tests, mock async dependencies, and avoid the most common pitfalls—so you can ship reliable async code with confidence.
Why pytest Needs pytest-asyncio
Standard pytest runs test functions synchronously. When you define:
async def test_fetch_user():
result = await fetch_user(1)
assert result.name == "Alice"
pytest sees test_fetch_user() as a function that returns a coroutine. It calls the function, gets the coroutine object, and moves on—never awaiting it. Your test “passes” even if fetch_user crashes, because the exception happens inside the unawaited coroutine.
pytest-asyncio solves this by:
- Wrapping async test functions in an event loop
- Automatically awaiting coroutines
- Managing setup/teardown for async fixtures
- Providing
AsyncMockfor mocking async behavior
Without it, async tests are effectively broken.
Getting Started: Installation and Configuration
First, install the plugin:
pip install pytest pytest-asyncio
Now, configure pytest to recognize async tests. The easiest approach is to set asyncio_mode = auto in your pytest.ini:
# pytest.ini
[pytest]
asyncio_mode = auto
With auto mode, pytest-asyncio automatically detects any async def test_* function and treats it as an async test. You don’t need to add markers manually.
Alternatively, you can configure this in pyproject.toml:
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
The asyncio_default_fixture_loop_scope = "function" setting (new in pytest-asyncio 0.23+) ensures each test gets its own event loop, improving isolation and reducing side effects.
If you prefer stricter control, use strict mode instead:
[pytest]
asyncio_mode = strict
In strict mode, you must explicitly mark every async test with @pytest.mark.asyncio. This is safer for large projects but adds overhead.
Writing Your First Async Test
With auto mode enabled, you can write async tests just like regular ones—just add async and await:
import pytest
async def fetch_user(user_id: int) -> dict:
# Simulate an async API call
return {"id": user_id, "name": "Alice"}
async def test_fetch_user():
result = await fetch_user(1)
assert result["name"] == "Alice"
Run it with:
pytest
pytest-asyncio will:
- Create an event loop
- Run
test_fetch_user()as a coroutine - Await the result
- Assert the value
No extra decorators needed in auto mode.
If you’re using strict mode, add the marker:
@pytest.mark.asyncio
async def test_fetch_user_strict():
result = await fetch_user(1)
assert result["name"] == "Alice"
Mocking Async Dependencies
Real async code often depends on external services: databases, APIs, queues. You can’t (and shouldn’t) call them in tests. Instead, mock them with AsyncMock.
pytest-asyncio provides AsyncMock from unittest.mock, which behaves like a regular Mock but supports await:
from unittest.mock import AsyncMock
import pytest
async def get_user_from_db(user_id: int, db_session):
result = await db_session.query("users").where("id = ?", user_id)
return result
async def test_get_user_from_db():
mock_db = AsyncMock()
mock_db.query.return_value.where.return_value = {"id": 1, "name": "Bob"}
user = await get_user_from_db(1, mock_db)
assert user["name"] == "Bob"
mock_db.query.assert_called_once()
AsyncMock ensures that when you await mock_db.query(...), it returns a coroutine that resolves to the value you specified. This is critical—using a regular Mock would cause await to fail.
Async Fixtures for Real Resources
Sometimes you need to test with real async resources (e.g., a database connection or HTTP client). pytest-asyncio lets you write async fixtures that await during setup and teardown.
Use @pytest_asyncio.fixture for async fixtures (required in strict mode; optional in auto):
import pytest_asyncio
import aiohttp
@pytest_asyncio.fixture
async def client():
async with aiohttp.ClientSession() as session:
yield session
# Teardown happens after yield
async def test_fetch_with_client(client):
async with client.get("https://api.example.com/data") as resp:
data = await resp.json()
assert "status" in data
The fixture:
- Creates an async resource (
ClientSession) - Yields it to the test
- Cleans up after the test completes
In auto mode, you can also use the standard @pytest.fixture on async functions—pytest-asyncio handles it automatically.
Common Pitfalls and How to Avoid Them
1. Silent Test Passes
Without pytest-asyncio, async tests pass even when they crash. Always verify your plugin is installed and configured.
2. Blocking Code in Async Tests
Avoid calling synchronous ORM methods or I/O inside async functions. This defeats asyncio’s benefits and can cause erratic behavior [13]. Use multiprocessing or async-aware libraries instead.
3. Shared Event Loops
By default, each test gets its own loop. If you share a loop across tests (e.g., via class-level fixtures), you may get race conditions. Stick to function scope unless you need explicit sharing [6].
4. Missing Markers in Strict Mode
If you use strict mode and forget @pytest.mark.asyncio, your test will be skipped. Double-check markers in strict projects [3].
5. Hanging CI Tests
Add a global timeout to prevent tests from hanging forever:
[pytest]
asyncio_mode = auto
timeout = 30
Use pytest-timeout for this feature.
Actionable Checklist: Test Async Code Today
Here’s what to do right now:
-
Install:
pip install pytest pytest-asyncio -
Configure: Add
asyncio_mode = autotopytest.iniorpyproject.toml -
Write: Create an
async def test_...function withawait -
Mock: Use
AsyncMockfor async dependencies -
Fixture: Add async fixtures for real resources with
@pytest_asyncio.fixture -
Timeout: Set
timeout = 30to avoid hanging CI -
Run: Execute
pytestand verify all tests are actually awaited
Final Thoughts
Testing async code isn’t harder—it just needs the right tool. pytest-asyncio removes the friction of event loops, makes await work naturally in tests, and gives you AsyncMock for clean dependency mocking. Once you configure it, your async tests become as straightforward as your sync ones.
Don’t let untested async logic become a bug factory. Start adding async def test_... functions today, and let pytest-asyncio handle the rest.
Try it now: Take one async function from your project, write a test with await, and run pytest. If it passes, you’re ready. If it hangs or skips, check your asyncio_mode setting.
What’s the first async function you’ll test? Share your code snippet in the comments—I’d love to see what you build.
If you found this helpful, consider buying me a coffee ☕ — it keeps these articles coming!
Also check out my AI tools collection: AI 次元世界 — free AI tools for developers.
Top comments (0)