Pipelinen parannuksia building blockeilla
This commit is contained in:
291
zipit/projekti_clean/prompts/6_qa_test_main.py.md
Normal file
291
zipit/projekti_clean/prompts/6_qa_test_main.py.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user