Skip to content

Commit 8bf3140

Browse files
CopilotSupraSummus
andauthored
Add example model step to FastAPI test setup (#7)
* Initial plan * Add example model step for FastAPI test setup Co-authored-by: SupraSummus <15822143+SupraSummus@users.noreply.github.com> * Address review: test_create asserts db entry, test_get seeds db directly Co-authored-by: SupraSummus <15822143+SupraSummus@users.noreply.github.com> * Rewrite example model step to use real PostgreSQL with SQLAlchemy Co-authored-by: SupraSummus <15822143+SupraSummus@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: SupraSummus <15822143+SupraSummus@users.noreply.github.com>
1 parent 169a232 commit 8bf3140

3 files changed

Lines changed: 167 additions & 0 deletions

File tree

.github/workflows/test.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ jobs:
1717
- run: ./test.sh
1818
working-directory: flask
1919
- run: ./fastapi/test.sh
20+
env:
21+
DATABASE_URL: postgresql+asyncpg://postgres:postgrespass@localhost:5432/postgres
2022

2123
services:
2224
postgres:

fastapi/35-example-model.sh

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/bin/bash
2+
3+
# This step adds an example SQLAlchemy model with CRUD routes and tests
4+
# using a real PostgreSQL database.
5+
# It is NOT included in everything.sh — it exists solely to verify
6+
# that the scaffolded app can easily be extended with new models.
7+
8+
# Determine project name if not set
9+
if [[ -z "$FASTAPI_PROJECT_NAME" ]]; then
10+
FASTAPI_PROJECT_NAME=$(basename "$PWD")
11+
FASTAPI_PROJECT_NAME=${FASTAPI_PROJECT_NAME//[-.]/_}
12+
export FASTAPI_PROJECT_NAME
13+
fi
14+
15+
# Install pytest-asyncio for async test support
16+
poetry add --group dev 'pytest-asyncio==*'
17+
18+
# Create example SQLAlchemy model with routes
19+
cat > "$FASTAPI_PROJECT_NAME/example.py" <<EOF
20+
from fastapi import APIRouter, Depends
21+
from pydantic import BaseModel
22+
from sqlalchemy import String, select
23+
from sqlalchemy.ext.asyncio import AsyncSession
24+
from sqlalchemy.orm import Mapped, mapped_column
25+
26+
from .database import Base, get_db
27+
28+
router = APIRouter(prefix="/examples", tags=["examples"])
29+
30+
31+
class Example(Base):
32+
__tablename__ = "examples"
33+
34+
id: Mapped[int] = mapped_column(primary_key=True, index=True)
35+
name: Mapped[str] = mapped_column(String, index=True)
36+
description: Mapped[str | None] = mapped_column(String, nullable=True)
37+
38+
39+
class ExampleCreate(BaseModel):
40+
name: str
41+
description: str | None = None
42+
43+
44+
class ExampleRead(BaseModel):
45+
id: int
46+
name: str
47+
description: str | None = None
48+
49+
model_config = {"from_attributes": True}
50+
51+
52+
@router.post("", response_model=ExampleRead)
53+
async def create_example(item: ExampleCreate, db: AsyncSession = Depends(get_db)):
54+
example = Example(**item.model_dump())
55+
db.add(example)
56+
await db.commit()
57+
await db.refresh(example)
58+
return example
59+
60+
61+
@router.get("", response_model=list[ExampleRead])
62+
async def list_examples(db: AsyncSession = Depends(get_db)):
63+
result = await db.execute(select(Example))
64+
return result.scalars().all()
65+
EOF
66+
67+
# Register example router in main.py
68+
sed -i '/^from fastapi import FastAPI/a from .example import router as example_router' "$FASTAPI_PROJECT_NAME/main.py"
69+
sed -i '/^app = FastAPI()/a app.include_router(example_router)' "$FASTAPI_PROJECT_NAME/main.py"
70+
71+
# Import example models in alembic env so migrations pick them up
72+
sed -i "/from $FASTAPI_PROJECT_NAME.models import/a from $FASTAPI_PROJECT_NAME.example import * # noqa: F401, F403" "alembic/env.py"
73+
74+
# Create conftest.py with async DB fixtures
75+
cat > "conftest.py" <<EOF
76+
import pytest
77+
from httpx import ASGITransport, AsyncClient
78+
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
79+
from sqlalchemy.orm import sessionmaker
80+
81+
from $FASTAPI_PROJECT_NAME.config import settings
82+
from $FASTAPI_PROJECT_NAME.database import Base, get_db
83+
from $FASTAPI_PROJECT_NAME.main import app
84+
85+
86+
@pytest.fixture
87+
async def db():
88+
engine = create_async_engine(settings.database_url)
89+
async with engine.begin() as conn:
90+
await conn.run_sync(Base.metadata.create_all)
91+
92+
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
93+
async with async_session() as session:
94+
yield session
95+
96+
async with engine.begin() as conn:
97+
await conn.run_sync(Base.metadata.drop_all)
98+
await engine.dispose()
99+
100+
101+
@pytest.fixture
102+
async def client(db):
103+
async def override_get_db():
104+
yield db
105+
106+
app.dependency_overrides[get_db] = override_get_db
107+
async with AsyncClient(
108+
transport=ASGITransport(app=app),
109+
base_url="http://test",
110+
) as ac:
111+
yield ac
112+
app.dependency_overrides.clear()
113+
EOF
114+
115+
# Replace test file with async tests using real DB
116+
cat > "test_app.py" <<EOF
117+
from sqlalchemy import select
118+
119+
from $FASTAPI_PROJECT_NAME.example import Example
120+
121+
122+
async def test_root(client):
123+
response = await client.get("/")
124+
assert response.status_code == 200
125+
assert response.json() == {"message": "Hello, World!"}
126+
127+
128+
async def test_health(client):
129+
response = await client.get("/health")
130+
assert response.status_code == 200
131+
assert response.json() == {"status": "healthy"}
132+
133+
134+
async def test_create_example(client, db):
135+
response = await client.post(
136+
"/examples", json={"name": "test", "description": "a test item"},
137+
)
138+
assert response.status_code == 200
139+
data = response.json()
140+
assert data["name"] == "test"
141+
assert data["description"] == "a test item"
142+
assert "id" in data
143+
144+
result = await db.execute(select(Example).where(Example.id == data["id"]))
145+
assert result.scalar_one().name == "test"
146+
147+
148+
async def test_list_examples(client, db):
149+
db.add(Example(name="list test"))
150+
await db.commit()
151+
152+
response = await client.get("/examples")
153+
assert response.status_code == 200
154+
items = response.json()
155+
assert isinstance(items, list)
156+
assert any(item["name"] == "list test" for item in items)
157+
EOF
158+
159+
# Configure pytest-asyncio mode
160+
sed -i '/\[tool\.pytest\.ini_options\]/a asyncio_mode = "auto"' pyproject.toml
161+
162+
poetry run isort .
163+
git add --all
164+
git commit -m "Add example model to demonstrate app extensibility"

fastapi/test.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pushd "$tmp_dir"
1515

1616
# Execute the pipeline script from the original directory
1717
"$original_dir/fastapi/everything.sh"
18+
source "$original_dir/fastapi/35-example-model.sh"
1819

1920
poetry run flake8
2021
cp example.env .env

0 commit comments

Comments
 (0)