Setup (CRITICAL)
Set asyncio_mode = "auto" in pyproject.toml to auto-detect async tests without markers. (The default "strict" mode also works if you mark tests with @pytest.mark.asyncio.)
See examples.md for the complete conftest.py and pyproject.toml setup.
Test Client
Use httpx.AsyncClient + ASGITransport — never TestClient for async tests.
See examples.md for client fixture setup.
Required Coverage Per Endpoint
- Happy path (create, read, update, delete)
- Not found (404)
- Validation error (422)
- Auth failure (401) — if protected
- Soft delete — deleted records not returned
Test Naming
async def test_create_item(): # Happy path
async def test_create_item_missing_name(): # Validation
async def test_get_item_not_found(): # 404
async def test_get_item_soft_deleted(): # Soft delete
Test Data Factories
Use simple factory functions with sensible defaults. Override only what the test cares about.
See examples.md for factory patterns.
Running Tests
pytest # All tests
pytest tests/test_items.py # One file
pytest -k "test_create" # By name pattern
pytest --tb=short -q # Quiet output
Gotchas
-
Missing
asyncio_mode = "auto"silently breaks everything. Tests appear to pass (0 collected) or hang indefinitely. Add[tool.pytest.ini_options] asyncio_mode = "auto"topyproject.tomlbefore writing any async test. This is the #1 cause of "my tests don't run." -
TestClientandAsyncClientare not interchangeable.TestClient(from Starlette) is synchronous — it blocks.AsyncClient(from httpx) is async — it uses the event loop. If your app usesasync defroutes withawait, you must useAsyncClientwithASGITransport. UsingTestClientmay hide async bugs because it runs synchronously. -
Database state leaks between tests without proper cleanup. Each test needs a clean database. Use an
autouse=Truefixture that creates/drops tables, or truncate between tests. Without this, test order matters and random failures appear in CI. -
response.json()returns snake_case from FastAPI by default. If your Pydantic models usealias_generator, the JSON keys may differ from your Python field names. Always assert against the actual JSON keys, not the Python attribute names. -
Forgetting to
awaitin async tests gives confusing errors. If you see<coroutine object ...>in test output instead of actual data, you forgot anawait. Everyclient.get(),client.post(), etc. must be awaited.