Skip to main content
fastapi-testing Configure pytest-asyncio with test database fixtures and integration tests for FastAPI routers
Ir para a instalação Skills Marketplace Descubra e explore skills de IA criadas pela comunidade.
Ocupações relacionadas SOC
Baseado na classificação ocupacional SOC
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Copiar promptMostrar detalhes do prompt Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
npx skills add https://github.com/agusmdev/fullstack-ai-template --skill fastapi-testingO comando permanece em uma só linha. Role horizontalmente para revisá-lo antes de copiar.
Prefere uma cópia local? Baixe os arquivos disponíveis atualmente no SkillsMP.
Baixar Zip Baixando... Mais deste repositório Codebase health scanner and technical debt tracker. Use when the user asks about code quality, technical debt, dead code, large files, god classes, duplicate functions, code smells, naming issues, import cycles, or coupling problems. Also use when asked for a health score, what to fix next, or to create a cleanup plan. Supports 29 languages.
Configure Alembic for async SQLAlchemy migrations with PostgreSQL
Create FastAPI application factory with lifespan, middleware, pagination, and router configuration
name fastapi-testing description Configure pytest-asyncio with test database fixtures and integration tests for FastAPI routers
FastAPI Testing Setup
Overview
This skill covers setting up pytest-asyncio for integration testing FastAPI routers with a test database.
Test Configuration in pyproject.toml
Ensure pyproject.toml has:
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
testpaths = ["tests" ]
pythonpath = ["src" ]
Add Test Database URL to Config
Update src/app/config.py:
from pydantic import PostgresDsn
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings (BaseSettings ):
model_config = SettingsConfigDict(
env_file=".env" ,
env_file_encoding="utf-8" ,
extra="ignore" ,
case_sensitive=False ,
)
test_database_url: PostgresDsn | None = None
settings = Settings()
Update .env.example:
TEST_DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/dbname_test
Create tests/conftest.py
Create tests/conftest.py:
import asyncio
from collections.abc import AsyncGenerator, Generator
import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from app.config import settings
from app.core.models import Base
from app.dependencies import get_db
from app.main import app
def get_test_database_url () -> str :
"""Get the test database URL."""
if settings.test_database_url:
return str (settings.test_database_url)
main_url = str (settings.database_url)
return main_url.replace("/dbname" , "/dbname_test" )
test_engine = create_async_engine(
get_test_database_url(),
echo=False ,
pool_pre_ping=True ,
)
test_async_session_factory = async_sessionmaker(
bind=test_engine,
class_=AsyncSession,
expire_on_commit=False ,
autocommit=False ,
autoflush=False ,
)
@pytest.fixture(scope="session" )
def event_loop () -> Generator[asyncio.AbstractEventLoop, None , ]:
loop = asyncio.get_event_loop_policy().new_event_loop()
loop
loop.close()
() -> AsyncGenerator[ , ]:
app.items.models Item
test_engine.begin() conn:
conn.run_sync(Base.metadata.create_all)
test_engine.begin() conn:
conn.run_sync(Base.metadata.drop_all)
test_engine.dispose()
() -> AsyncGenerator[AsyncSession, ]:
test_async_session_factory() session:
session
session.rollback()
( ) -> AsyncGenerator[AsyncClient, ]:
() -> AsyncGenerator[AsyncSession, ]:
db_session
app.dependency_overrides[get_db] = override_get_db
AsyncClient(
transport=ASGITransport(app=app),
base_url= ,
) ac:
ac
app.dependency_overrides.clear()
() -> :
{
: ,
: ,
}
( ) -> :
response = client.post( , json=sample_item_data)
response.status_code ==
response.json()
Create Test Files
tests/api/init .py
tests/api/v1/init .py
tests/api/v1/test_items.py
Create tests/api/v1/test_items.py:
import pytest
from httpx import AsyncClient
class TestCreateItem :
"""Tests for POST /api/v1/items"""
async def test_create_item_success (
self,
client: AsyncClient,
sample_item_data: dict ,
) -> None :
"""Test successful item creation."""
response = await client.post("/api/v1/items" , json=sample_item_data)
assert response.status_code == 201
data = response.json()
assert data["name" ] == sample_item_data["name" ]
assert data["description" ] == sample_item_data["description" ]
assert "id" in data
assert "created_at" in data
assert "updated_at" in data
async def test_create_item_duplicate_name (
self,
client: AsyncClient,
created_item: dict ,
sample_item_data: dict ,
) -> None :
"""Test that creating item with duplicate name fails."""
response = await client.post("/api/v1/items" , json=sample_item_data)
assert response.status_code == 409
data = response.json()
data[ ] ==
( ) -> :
response = client.post( , json={ : })
response.status_code ==
data = response.json()
data[ ] ==
( ) -> :
response = client.post( , json={ : })
response.status_code ==
:
( ) -> :
item_id = created_item[ ]
response = client.get( )
response.status_code ==
data = response.json()
data[ ] == item_id
data[ ] == created_item[ ]
( ) -> :
fake_id =
response = client.get( )
response.status_code ==
data = response.json()
data[ ] ==
( ) -> :
response = client.get( )
response.status_code ==
:
( ) -> :
response = client.get( )
response.status_code ==
data = response.json()
data[ ] == []
data[ ] ==
( ) -> :
response = client.get( )
response.status_code ==
data = response.json()
(data[ ]) >=
data[ ] >=
( ) -> :
response = client.get( )
response.status_code ==
data = response.json()
data
data
data[ ] ==
data[ ] ==
( ) -> :
name = created_item[ ]
response = client.get( )
response.status_code ==
data = response.json()
(item[ ] == name item data[ ])
( ) -> :
response = client.get( )
response.status_code ==
data = response.json()
( item[ ].lower() item data[ ])
:
( ) -> :
item_id = created_item[ ]
update_data = { : }
response = client.patch( , json=update_data)
response.status_code ==
data = response.json()
data[ ] ==
data[ ] == created_item[ ]
( ) -> :
item_id = created_item[ ]
update_data = { : }
response = client.patch( , json=update_data)
response.status_code ==
data = response.json()
data[ ] == created_item[ ]
data[ ] ==
( ) -> :
fake_id =
response = client.patch( , json={ : })
response.status_code ==
:
( ) -> :
item_id = created_item[ ]
response = client.delete( )
response.status_code ==
get_response = client.get( )
get_response.status_code ==
( ) -> :
fake_id =
response = client.delete( )
response.status_code ==
:
( ) -> :
item_id = created_item[ ]
client.delete( )
response = client.post( )
response.status_code ==
data = response.json()
data[ ] == item_id
get_response = client.get( )
get_response.status_code ==
Running Tests
uv run pytest
uv run pytest -v
uv run pytest tests/api/v1/test_items.py
uv run pytest tests/api/v1/test_items.py::TestCreateItem
uv run pytest tests/api/v1/test_items.py::TestCreateItem::test_create_item_success
uv run pytest --cov=app --cov-report=html
uv run pytest -k "create"
Additional Test Fixtures
Factory Fixtures
@pytest.fixture
def item_factory ():
"""Factory for creating item data with unique names."""
counter = 0
def _factory (**overrides ):
nonlocal counter
counter += 1
defaults = {
"name" : f"Item {counter} " ,
"description" : f"Description {counter} " ,
}
defaults.update(overrides)
return defaults
return _factory
async def test_with_factory (client: AsyncClient, item_factory ):
"""Test using factory fixture."""
item1 = await client.post("/api/v1/items" , json=item_factory())
item2 = await client.post("/api/v1/items" , json=item_factory())
assert item1.json()["name" ] != item2.json()["name" ]
Multiple Items Fixture
@pytest.fixture
async def multiple_items (
client: AsyncClient,
item_factory,
) -> list [dict ]:
"""Create multiple items for testing."""
items = []
for i in range (5 ):
response = await client.post("/api/v1/items" , json=item_factory())
items.append(response.json())
return items
Test Database Setup
Before running tests, create the test database:
createdb dbname_test
psql -c "CREATE DATABASE dbname_test;"
Testing Best Practices
Test isolation : Each test should be independent
Use fixtures : Share setup code via fixtures
Test edge cases : Empty data, invalid input, not found
Test error responses : Verify error codes and messages
Descriptive names : Test names should describe the scenario
One assertion focus : Each test should focus on one behavior
None
"""
Create an event loop for the test session.
This fixture is required for session-scoped async fixtures.
"""
yield
@pytest.fixture(scope="session" , autouse=True )
async
def
setup_database
None
None
"""
Set up the test database schema.
Creates all tables before tests run and drops them after.
Runs once per test session.
"""
from
import
async
with
as
await
yield
async
with
as
await
await
@pytest.fixture
async
def
db_session
None
"""
Provide a database session for a single test.
Each test gets its own session with automatic rollback.
This ensures test isolation.
"""
async
with
as
yield
await
@pytest.fixture
async
def
client
db_session: AsyncSession
None
"""
Provide an async HTTP client for testing API endpoints.
Overrides the database dependency to use the test session.
"""
async
def
override_get_db
None
yield
async
with
"http://test"
as
yield
@pytest.fixture
def
sample_item_data
dict
"""Provide sample data for creating an item."""
return
"name"
"Test Item"
"description"
"A test item description"
@pytest.fixture
async
def
created_item
client: AsyncClient,
sample_item_data: dict ,
dict
"""Create an item and return the response data."""
await
"/api/v1/items"
assert
201
return
assert
"error_code"
"CONFLICT"
async
def
test_create_item_missing_name
self,
client: AsyncClient,
None
"""Test that name is required."""
await
"/api/v1/items"
"description"
"test"
assert
422
assert
"error_code"
"VALIDATION_ERROR"
async
def
test_create_item_empty_name
self,
client: AsyncClient,
None
"""Test that empty name is rejected."""
await
"/api/v1/items"
"name"
""
assert
422
class
TestGetItem
"""Tests for GET /api/v1/items/{id}"""
async
def
test_get_item_success
self,
client: AsyncClient,
created_item: dict ,
None
"""Test successful item retrieval."""
"id"
await
f"/api/v1/items/{item_id} "
assert
200
assert
"id"
assert
"name"
"name"
async
def
test_get_item_not_found
self,
client: AsyncClient,
None
"""Test 404 for non-existent item."""
"00000000-0000-0000-0000-000000000000"
await
f"/api/v1/items/{fake_id} "
assert
404
assert
"error_code"
"NOT_FOUND"
async
def
test_get_item_invalid_uuid
self,
client: AsyncClient,
None
"""Test 422 for invalid UUID format."""
await
"/api/v1/items/not-a-uuid"
assert
422
class
TestListItems
"""Tests for GET /api/v1/items"""
async
def
test_list_items_empty
self,
client: AsyncClient,
None
"""Test listing items when empty."""
await
"/api/v1/items"
assert
200
assert
"items"
assert
"total"
0
async
def
test_list_items_with_data
self,
client: AsyncClient,
created_item: dict ,
None
"""Test listing items with data."""
await
"/api/v1/items"
assert
200
assert
len
"items"
1
assert
"total"
1
async
def
test_list_items_pagination
self,
client: AsyncClient,
created_item: dict ,
None
"""Test pagination parameters."""
await
"/api/v1/items?page=1&size=10"
assert
200
assert
"page"
in
assert
"size"
in
assert
"page"
1
assert
"size"
10
async
def
test_list_items_filter_by_name
self,
client: AsyncClient,
created_item: dict ,
None
"""Test filtering by name."""
"name"
await
f"/api/v1/items?name={name} "
assert
200
assert
all
"name"
for
in
"items"
async
def
test_list_items_filter_ilike
self,
client: AsyncClient,
created_item: dict ,
None
"""Test case-insensitive filtering."""
await
"/api/v1/items?name__ilike=test"
assert
200
assert
all
"test"
in
"name"
for
in
"items"
class
TestUpdateItem
"""Tests for PATCH /api/v1/items/{id}"""
async
def
test_update_item_success
self,
client: AsyncClient,
created_item: dict ,
None
"""Test successful item update."""
"id"
"name"
"Updated Name"
await
f"/api/v1/items/{item_id} "
assert
200
assert
"name"
"Updated Name"
assert
"description"
"description"
async
def
test_update_item_partial
self,
client: AsyncClient,
created_item: dict ,
None
"""Test partial update (only description)."""
"id"
"description"
"New description"
await
f"/api/v1/items/{item_id} "
assert
200
assert
"name"
"name"
assert
"description"
"New description"
async
def
test_update_item_not_found
self,
client: AsyncClient,
None
"""Test update non-existent item."""
"00000000-0000-0000-0000-000000000000"
await
f"/api/v1/items/{fake_id} "
"name"
"test"
assert
404
class
TestDeleteItem
"""Tests for DELETE /api/v1/items/{id}"""
async
def
test_delete_item_success
self,
client: AsyncClient,
created_item: dict ,
None
"""Test successful item deletion (soft delete)."""
"id"
await
f"/api/v1/items/{item_id} "
assert
204
await
f"/api/v1/items/{item_id} "
assert
404
async
def
test_delete_item_not_found
self,
client: AsyncClient,
None
"""Test delete non-existent item."""
"00000000-0000-0000-0000-000000000000"
await
f"/api/v1/items/{fake_id} "
assert
404
class
TestRestoreItem
"""Tests for POST /api/v1/items/{id}/restore"""
async
def
test_restore_item_success
self,
client: AsyncClient,
created_item: dict ,
None
"""Test restoring a soft-deleted item."""
"id"
await
f"/api/v1/items/{item_id} "
await
f"/api/v1/items/{item_id} /restore"
assert
200
assert
"id"
await
f"/api/v1/items/{item_id} "
assert
200