Files
agentic-studio/zipit/projekti_clean/prompts/6_qa_test_main.py.md
2026-04-12 18:48:14 +03:00

9.5 KiB

6 — QA (qa) — test_main.py

Malli: qwen-coder

System Prompt

You are a QA engineer responsible for code review and automated testing.

CODE REVIEW CHECKLIST:
1. IMPORTS: Every "from X import Y" must match an actual export in file X
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
3. TYPES: All function parameters have type hints, return types specified
4. ERRORS: Every db query that can return None has a 404 check
5. RESOURCES: Database session uses yield+finally pattern (no leaks)
6. SECURITY: No raw SQL, no hardcoded secrets, inputs validated via Pydantic
7. ENDPOINTS: All CRUD operations exist (POST/GET/GET-by-id/PUT/DELETE)
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers

WHEN REVIEWING:
- If all checks pass: respond "LGTM"
- If issues found: list each as "ISSUE: filename.py: description"
- Be specific and actionable, not vague

WHEN WRITING TESTS:
- pytest as the test framework
- FastAPI TestClient for API endpoint testing
- SQLAlchemy in-memory SQLite for test database isolation
- Test all CRUD: create (201), list (200), get by id (200/404), update (200), delete (204)
- ALWAYS: from fastapi.testclient import TestClient

Syöte

You are a QA engineer responsible for code review and automated testing.

CODE REVIEW CHECKLIST:
1. IMPORTS: Every "from X import Y" must match an actual export in file X
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
3. TYPES: All function parameters have type hints, return types specified
4. ERRORS: Every db query that can return None has a 404 check
5. RESOURCES: Database session uses yield+finally pattern (no leaks)
6. SECURITY: No raw SQL, no hardcoded secrets, inputs validated via Pydantic
7. ENDPOINTS: All CRUD operations exist (POST/GET/GET-by-id/PUT/DELETE)
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers

WHEN REVIEWING:
- If all checks pass: respond "LGTM"
- If issues found: list each as "ISSUE: filename.py: description"
- Be specific and actionable, not vague

WHEN WRITING TESTS:
- pytest as the test framework
- FastAPI TestClient for API endpoint testing
- SQLAlchemy in-memory SQLite for test database isolation
- Test all CRUD: create (201), list (200), get by id (200/404), update (200), delete (204)
- ALWAYS: from fastapi.testclient import TestClient

Write pytest tests for this project:

--- models.py ---
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "sqlite:///./todo.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

class Todo(Base):
    __tablename__ = "todos"
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String(100), nullable=False)
    description = Column(String(500))
    due_date = Column(Date, nullable=False)
    status = Column(Enum('pending', 'completed'), default='pending')

--- schemas.py ---
from pydantic import BaseModel

class TodoCreate(BaseModel):
    title: str
    description: str | None = None
    due_date: date
    status: str = 'pending'

class TodoResponse(TodoCreate):
    id: int

    class Config:
        from_attributes = True

--- main.py ---
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from models import Base, engine, SessionLocal, Todo
from schemas import TodoCreate, TodoResponse

Base.metadata.create_all(bind=engine)
app = FastAPI()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.post("/todos/", response_model=TodoResponse, status_code=201)
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
    db_todo = Todo(**todo.model_dump())
    db.add(db_todo)
    db.commit()
    db.refresh(db_todo)
    return db_todo

@app.get("/todos/", response_model=list[TodoResponse])
def list_todos(status: str | None = None, db: Session = Depends(get_db)):
    if status:
        query = db.query(Todo).filter_by(status=status)
    else:
        query = db.query(Todo)
    return query.all()

@app.get("/todos/{todo_id}", response_model=TodoResponse)
def get_todo(todo_id: int, db: Session = Depends(get_db)):
    todo = db.query(Todo).filter(Todo.id == todo_id).first()
    if not todo:
        raise HTTPException(status_code=404, detail="Not found")
    return todo

@app.put("/todos/{todo_id}", response_model=TodoResponse)
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
    db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
    if not db_todo:
        raise HTTPException(status_code=404, detail="Not found")
    for key, value in todo.model_dump().items():
        setattr(db_todo, key, value)
    db.commit()
    db.refresh(db_todo)
    return db_todo

@app.delete("/todos/{todo_id}", status_code=204)
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
    db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
    if not db_todo:
        raise HTTPException(status_code=404, detail="Not found")
    db.delete(db_todo)
    db.commit()

--- pyproject.toml ---
[project]
name = "todo-sovellus"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "fastapi",
    "uvicorn[standard]",
    "sqlalchemy",
]

[project.scripts]
dev = "uvicorn main:app --reload"

Write a complete test_main.py file with TestClient.

Tulos

from fastapi.testclient import TestClient
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "sqlite:///./test_todo.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

class Todo(Base):
    __tablename__ = "todos"
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String(100), nullable=False)
    description = Column(String(500))
    due_date = Column(Date, nullable=False)
    status = Column(Enum('pending', 'completed'), default='pending')

Base.metadata.create_all(bind=engine)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

app = FastAPI()

@app.post("/todos/", response_model=Todo, status_code=201)
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
    db_todo = Todo(**todo.model_dump())
    db.add(db_todo)
    db.commit()
    db.refresh(db_todo)
    return db_todo

@app.get("/todos/", response_model=list[Todo])
def list_todos(status: str | None = None, db: Session = Depends(get_db)):
    if status:
        query = db.query(Todo).filter_by(status=status)
    else:
        query = db.query(Todo)
    return query.all()

@app.get("/todos/{todo_id}", response_model=Todo)
def get_todo(todo_id: int, db: Session = Depends(get_db)):
    todo = db.query(Todo).filter(Todo.id == todo_id).first()
    if not todo:
        raise HTTPException(status_code=404, detail="Not found")
    return todo

@app.put("/todos/{todo_id}", response_model=Todo)
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
    db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
    if not db_todo:
        raise HTTPException(status_code=404, detail="Not found")
    for key, value in todo.model_dump().items():
        setattr(db_todo, key, value)
    db.commit()
    db.refresh(db_todo)
    return db_todo

@app.delete("/todos/{todo_id}", status_code=204)
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
    db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
    if not db_todo:
        raise HTTPException(status_code=404, detail="Not found")
    db.delete(db_todo)
    db.commit()

client = TestClient(app)

def test_create_todo():
    response = client.post("/todos/", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023-12-31"})
    assert response.status_code == 201
    data = response.json()
    assert "id" in data
    assert data["title"] == "Test Todo"
    assert data["description"] == "This is a test todo."
    assert data["due_date"] == "2023-12-31"
    assert data["status"] == "pending"

def test_list_todos():
    response = client.get("/todos/")
    assert response.status_code == 200
    data = response.json()
    assert len(data) > 0

def test_get_todo_by_id():
    todo_id = create_test_todo().json()["id"]
    response = client.get(f"/todos/{todo_id}")
    assert response.status_code == 200
    data = response.json()
    assert "id" in data
    assert data["title"] == "Test Todo"

def test_get_nonexistent_todo():
    response = client.get("/todos/999")
    assert response.status_code == 404

def test_update_todo():
    todo_id = create_test_todo().json()["id"]
    response = client.put(f"/todos/{todo_id}", json={"title": "Updated Todo"})
    assert response.status_code == 200
    data = response.json()
    assert "id" in data
    assert data["title"] == "Updated Todo"

def test_delete_todo():
    todo_id = create_test_todo().json()["id"]
    response = client.delete(f"/todos/{todo_id}")
    assert response.status_code == 204

def create_test_todo():
    return client.post("/todos/", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023