# API Testing ## Table of Contents - [FastAPI Testing](#fastapi-testing) - [Flask Testing](#flask-testing) - [Test Factories](#test-factories) - [Authentication Testing](#authentication-testing) - [Database Testing](#database-testing) ## FastAPI Testing ### Setup ```python # tests/conftest.py import pytest from httpx import AsyncClient, ASGITransport from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlmodel import SQLModel from app.main import app from app.core.database import get_session TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db" test_engine = create_async_engine(TEST_DATABASE_URL) TestSession = async_sessionmaker(test_engine, class_=AsyncSession) @pytest.fixture(scope="function") async def db_session(): """Fresh database for each test.""" async with test_engine.begin() as conn: await conn.run_sync(SQLModel.metadata.create_all) async with TestSession() as session: yield session async with test_engine.begin() as conn: await conn.run_sync(SQLModel.metadata.drop_all) @pytest.fixture async def client(db_session): """Test client with dependency override.""" async def override_session(): yield db_session app.dependency_overrides[get_session] = override_session async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test", ) as ac: yield ac app.dependency_overrides.clear() ``` ### Basic Endpoint Tests ```python import pytest from httpx import AsyncClient class TestUsers: @pytest.mark.asyncio async def test_create_user(self, client: AsyncClient): response = await client.post( "/api/users", json={"email": "test@example.com", "name": "Test User"}, ) assert response.status_code == 201 data = response.json() assert data["email"] == "test@example.com" assert "id" in data @pytest.mark.asyncio async def test_get_user(self, client: AsyncClient, test_user): response = await client.get(f"/api/users/{test_user.id}") assert response.status_code == 200 assert response.json()["id"] == test_user.id @pytest.mark.asyncio async def test_get_user_not_found(self, client: AsyncClient): response = await client.get("/api/users/99999") assert response.status_code == 404 @pytest.mark.asyncio async def test_update_user(self, client: AsyncClient, test_user, auth_headers): response = await client.patch( f"/api/users/{test_user.id}", json={"name": "Updated Name"}, headers=auth_headers, ) assert response.status_code == 200 assert response.json()["name"] == "Updated Name" @pytest.mark.asyncio async def test_delete_user(self, client: AsyncClient, test_user, admin_headers): response = await client.delete( f"/api/users/{test_user.id}", headers=admin_headers, ) assert response.status_code == 204 ``` ### Testing Query Parameters ```python @pytest.mark.asyncio async def test_list_users_pagination(self, client: AsyncClient, auth_headers): response = await client.get( "/api/users", params={"page": 1, "per_page": 10, "sort": "-created_at"}, headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert "items" in data assert "total" in data assert data["page"] == 1 @pytest.mark.asyncio async def test_search_users(self, client: AsyncClient, auth_headers): response = await client.get( "/api/users", params={"q": "john", "active": True}, headers=auth_headers, ) assert response.status_code == 200 ``` ### Testing File Uploads ```python import io @pytest.mark.asyncio async def test_upload_file(self, client: AsyncClient, auth_headers): file_content = b"Hello, World!" files = {"file": ("test.txt", io.BytesIO(file_content), "text/plain")} response = await client.post( "/api/files/upload", files=files, headers=auth_headers, ) assert response.status_code == 201 assert response.json()["filename"] == "test.txt" ``` ## Flask Testing ### Setup ```python import pytest from app import create_app from app.extensions import db @pytest.fixture def app(): """Create application for testing.""" app = create_app("testing") app.config["TESTING"] = True app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" with app.app_context(): db.create_all() yield app db.drop_all() @pytest.fixture def client(app): """Test client.""" return app.test_client() @pytest.fixture def runner(app): """CLI test runner.""" return app.test_cli_runner() ``` ### Flask Test Examples ```python def test_create_user(client): response = client.post( "/api/users", json={"email": "test@example.com", "name": "Test"}, ) assert response.status_code == 201 def test_get_user(client, test_user): response = client.get(f"/api/users/{test_user.id}") assert response.status_code == 200 assert response.json["email"] == test_user.email def test_protected_route(client, auth_token): response = client.get( "/api/protected", headers={"Authorization": f"Bearer {auth_token}"}, ) assert response.status_code == 200 ``` ## Test Factories ### Using factory_boy ```python # tests/factories.py import factory from faker import Faker from app.models import User, Project fake = Faker() class UserFactory(factory.Factory): class Meta: model = User email = factory.LazyAttribute(lambda _: fake.email()) name = factory.LazyAttribute(lambda _: fake.name()) hashed_password = "hashed_password" is_active = True class ProjectFactory(factory.Factory): class Meta: model = Project name = factory.LazyAttribute(lambda _: fake.company()) description = factory.LazyAttribute(lambda _: fake.sentence()) owner_id = None # Async helper async def create_user(session, **kwargs) -> User: user = UserFactory(**kwargs) session.add(user) await session.commit() await session.refresh(user) return user async def create_users(session, count: int = 5, **kwargs) -> list[User]: users = [UserFactory(**kwargs) for _ in range(count)] session.add_all(users) await session.commit() return users ``` ### Using Factories in Tests ```python from tests.factories import create_user, create_users @pytest.mark.asyncio async def test_list_users(client, db_session, auth_headers): # Create 20 users await create_users(db_session, count=20) response = await client.get("/api/users", headers=auth_headers) assert response.status_code == 200 assert response.json()["total"] >= 20 @pytest.mark.asyncio async def test_filter_active_users(client, db_session, auth_headers): await create_user(db_session, is_active=True) await create_user(db_session, is_active=False) response = await client.get( "/api/users", params={"active": True}, headers=auth_headers, ) data = response.json() assert all(u["is_active"] for u in data["items"]) ``` ## Authentication Testing ### Auth Fixtures ```python from app.core.security import create_access_token, hash_password @pytest.fixture async def test_user(db_session): """Create a regular test user.""" user = User( email="test@example.com", hashed_password=hash_password("password123"), is_active=True, ) db_session.add(user) await db_session.commit() await db_session.refresh(user) return user @pytest.fixture async def admin_user(db_session): """Create an admin user.""" user = User( email="admin@example.com", hashed_password=hash_password("admin123"), is_active=True, is_admin=True, ) db_session.add(user) await db_session.commit() await db_session.refresh(user) return user @pytest.fixture def auth_headers(test_user): """Headers with valid auth token.""" token = create_access_token({"sub": str(test_user.id)}) return {"Authorization": f"Bearer {token}"} @pytest.fixture def admin_headers(admin_user): """Headers with admin auth token.""" token = create_access_token({"sub": str(admin_user.id)}) return {"Authorization": f"Bearer {token}"} ``` ### Auth Test Examples ```python class TestAuth: @pytest.mark.asyncio async def test_login_success(self, client, test_user): response = await client.post( "/api/auth/login", data={"username": test_user.email, "password": "password123"}, ) assert response.status_code == 200 assert "access_token" in response.json() @pytest.mark.asyncio async def test_login_wrong_password(self, client, test_user): response = await client.post( "/api/auth/login", data={"username": test_user.email, "password": "wrong"}, ) assert response.status_code == 401 @pytest.mark.asyncio async def test_protected_endpoint_no_auth(self, client): response = await client.get("/api/protected") assert response.status_code == 401 @pytest.mark.asyncio async def test_admin_endpoint_regular_user(self, client, auth_headers): response = await client.delete( "/api/admin/users/1", headers=auth_headers, ) assert response.status_code == 403 ``` ## Database Testing ### Transaction Rollback Pattern ```python @pytest.fixture async def db_session(engine): """Each test runs in a transaction that rolls back.""" async with engine.connect() as conn: await conn.begin() async with AsyncSession(bind=conn) as session: yield session await conn.rollback() ``` ### Testing Repository Layer ```python from app.repositories import UserRepository class TestUserRepository: @pytest.mark.asyncio async def test_create(self, db_session): repo = UserRepository(db_session) user = await repo.create({ "email": "test@example.com", "hashed_password": "hash", }) assert user.id is not None assert user.email == "test@example.com" @pytest.mark.asyncio async def test_get_by_email(self, db_session, test_user): repo = UserRepository(db_session) user = await repo.get_by_email(test_user.email) assert user.id == test_user.id @pytest.mark.asyncio async def test_soft_delete(self, db_session, test_user): repo = UserRepository(db_session) await repo.soft_delete(test_user.id) user = await repo.get_by_id(test_user.id) assert user.status == 0 # Soft deleted ```