Skip to content
advanced Phase 16 · Python Testing Advanced

Async Testing

Test asynchronous code with pytest-asyncio and async fixtures.

1h
0 problems
Topic Progress 0%

Modes, Markers, and Event Loops

Modes, Markers, and Event Loops

Plain pytest cannot await coroutines — without a plugin, async tests are skipped with "async def functions are not natively supported". pytest-asyncio wraps each async test in an event loop.

# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"        # no markers needed; "strict" (default) requires them

In strict mode every coroutine test carries the marker:

import pytest

@pytest.mark.asyncio
async def test_fetch(client):
    resp = await client.get("/health")
    assert resp.status == 200

By default each test gets a function-scoped loop, created and closed around it — anything bound to one loop (clients, locks) must not leak into the next test. Unmarked async tests in strict mode are silently skipped, which is the most common "my tests never ran" bug.

Async Fixtures and Composition

Async Fixtures and Composition

Async fixtures need the plugin’s decorator in strict/standard mode so they are awaited on the right loop:

import pytest_asyncio

@pytest_asyncio.fixture
async def db_url(tmp_path):
    server = await start_db(tmp_path)     # awaited setup
    yield f"sqlite+aiosqlite:///{server}"
    await server.stop()                   # awaited teardown

@pytest_asyncio.fixture(loop_scope="session")
async def app_client():
    async with AsyncClient(app=app) as c:
        yield c

loop_scope (matching the test’s @pytest.mark.asyncio(loop_scope="session")) lets fixtures and tests share one loop across a module or session — necessary when a background task or connection pool must survive multiple tests. Keep the scope change deliberate: a session loop widens the blast radius of leaked tasks.

Pitfalls and Interview Angles

Pitfalls and Interview Angles

  • Coroutine never awaited. Creating a task and forgetting await/asyncio.gather yields a RuntimeWarning and a test that asserts nothing; enable filterwarnings = error to convert warnings into failures.
  • Loop mismatch. Binding an object on loop A and awaiting it on loop B raises "attached to a different loop" — scopes must match across fixture and test.
  • Order dependence. asyncio.create_task work continues until the loop closes; cancel or await tasks in teardown so tests stay isolated.
  • Timeouts. Pair with pytest-timeout so a hung await fails fast instead of stalling CI.

Interview angle: explain the loop lifecycle pytest-asyncio manages (create → run test → run teardown → close), why asyncio.run per test is the default isolation strategy, and how you would test code that assumes a long-lived loop.

Quiz

1. In strict mode, what happens to an unmarked async def test?

Question 1 options

2. Why does an async fixture use @pytest_asyncio.fixture instead of @pytest.fixture?

Question 2 options

3. Error "object attached to a different loop" usually means…

Question 3 options

Flashcards

Question

asyncio_mode

Answer

strict (default): @pytest.mark.asyncio required; auto: plugin wraps all async tests automatically.

Question

@pytest_asyncio.fixture

Answer

Declares an async fixture so setup/teardown coroutine steps run on the test’s loop.

Question

loop_scope

Answer

Chooses which loop a test/fixture binds to (function default; session for shared pools).

Question

Skipped async test symptom

Answer

Unmarked async def in strict mode never runs — check pytest’s skip warnings in CI.

Revision Notes

Key Takeaways

  • 1. pytest-asyncio runs each coroutine test inside a managed event loop; modes decide whether markers are mandatory
  • 2. Async fixtures need the plugin decorator, and loop scopes must match across fixture and test
  • 3. Make warnings errors so un-awaited coroutines fail loudly instead of passing vacuously

Interview Tips

  • • Explain the default function-scoped loop and why sharing loops across tests trades isolation for realism
  • • Describe diagnosing "tests pass but never executed": skip warnings plus --co to inspect collection

Cheat Sheet

asyncio_mode strict → @pytest.mark.asyncio; async fixtures → @pytest_asyncio.fixture; default loop scope function; match loop_scope; unmarked async tests skip.

Continue Learning