Claude
Skills
Sign in
Back

api-testing

Included with Lifetime
$97 forever

Use when writing tests for backend APIs or frontend flows. Triggers for: unit tests, integration tests, E2E tests, pytest fixtures, TestClient setup, mock data factories, or test coverage analysis. NOT for: testing business logic that doesn't involve API endpoints.

Web Devscripts

What this skill does


# API Testing Skill

Expert testing for FastAPI backends and React/Next.js frontends with unit, integration, and E2E test patterns.

## Quick Reference

| Test Type | Tool | Purpose | Scope |
|-----------|------|---------|-------|
| Unit | pytest | Pure functions, services | Isolated |
| Integration | pytest + TestClient | DB + auth + routes | Combined |
| E2E | Playwright/Cypress | Browser flows | Full stack |

## Project Structure

```
backend/
├── tests/
│   ├── __init__.py
│   ├── conftest.py              # Shared fixtures
│   ├── unit/
│   │   ├── test_services.py     # Business logic tests
│   │   └── test_utils.py        # Utility function tests
│   ├── integration/
│   │   ├── test_students.py     # Student API tests
│   │   ├── test_fees.py         # Fee API tests
│   │   └── test_auth.py         # Authentication tests
│   └── fixtures/
│       ├── students.json        # Test data
│       └── users.json
frontend/
├── e2e/
│   ├── specs/
│   │   ├── student.spec.ts
│   │   └── fee.spec.ts
│   ├── pages/
│   │   ├── DashboardPage.ts
│   │   └── StudentPage.ts
│   └── utils/
│       └── test-data.ts
└── playwright.config.ts
```

## Backend: Pytest Setup

### conftest.py (Shared Fixtures)

```python
# backend/tests/conftest.py
import pytest
from typing import Generator
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app.main import app
from app.db.database import get_db, Base
from app.models import User, Student
from app.auth.jwt import create_access_token
from passlib.context import CryptContext


# Test database setup
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
engine = create_engine(
    SQLALCHEMY_DATABASE_URL,
    connect_args={"check_same_thread": False},
    poolclass=StaticPool,
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)


@pytest.fixture(scope="function")
def db_session():
    """Create a fresh database for each test."""
    Base.metadata.create_all(bind=engine)
    session = TestingSessionLocal()
    try:
        yield session
    finally:
        session.close()
        Base.metadata.drop_all(bind=engine)


@pytest.fixture(scope="function")
def client(db_session):
    """Create a test client with database override."""

    def override_get_db():
        try:
            yield db_session
        finally:
            pass

    app.dependency_overrides[get_db] = override_get_db
    with TestClient(app) as test_client:
        yield test_client
    app.dependency_overrides.clear()


@pytest.fixture
def test_user(db_session):
    """Create a test user."""
    pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
    hashed_password = pwd_context.hash("testpassword123")

    user = User(
        email="[email protected]",
        hashed_password=hashed_password,
        full_name="Test User",
        is_active=True,
    )
    db_session.add(user)
    db_session.commit()
    db_session.refresh(user)
    return user


@pytest.fixture
def auth_token(test_user):
    """Generate JWT token for test user."""
    return create_access_token(data={"sub": test_user.email, "roles": ["admin"]})


@pytest.fixture
def auth_headers(auth_token):
    """Headers with authentication token."""
    return {"Authorization": f"Bearer {auth_token}"}
```

### Unit Tests (Pure Functions)

```python
# backend/tests/unit/test_services.py
import pytest
from app.services.fee_calculator import calculate_fee, FeeCalculationError


class TestCalculateFee:
    """Unit tests for fee calculation logic."""

    def test_basic_fee_calculation(self):
        """Test basic fee calculation without discounts."""
        result = calculate_fee(
            base_amount=1000.00,
            grade_level=9,
            has_sibling_discount=False,
            is_new_student=False,
        )
        assert result == 1000.00

    def test_sibling_discount(self):
        """Test 10% sibling discount."""
        result = calculate_fee(
            base_amount=1000.00,
            grade_level=9,
            has_sibling_discount=True,
            is_new_student=False,
        )
        assert result == 900.00

    def test_new_student_discount(self):
        """Test 15% new student discount."""
        result = calculate_fee(
            base_amount=1000.00,
            grade_level=9,
            has_sibling_discount=False,
            is_new_student=True,
        )
        assert result == 850.00

    def test_combined_discounts(self):
        """Test combined sibling and new student discounts."""
        result = calculate_fee(
            base_amount=1000.00,
            grade_level=9,
            has_sibling_discount=True,
            is_new_student=True,
        )
        # 10% + 15% = 25% discount
        assert result == 750.00

    def test_invalid_base_amount(self):
        """Test that negative amounts raise error."""
        with pytest.raises(FeeCalculationError):
            calculate_fee(
                base_amount=-100.00,
                grade_level=9,
                has_sibling_discount=False,
                is_new_student=False,
            )

    def test_grade_level_multipliers(self):
        """Test different grade level multipliers."""
        # Elementary (1-5): 1.0x
        assert calculate_fee(1000.00, grade_level=3) == 1000.00
        # Middle (6-8): 1.1x
        assert calculate_fee(1000.00, grade_level=7) == 1100.00
        # High (9-12): 1.2x
        assert calculate_fee(1000.00, grade_level=10) == 1200.00
```

### Integration Tests (API Endpoints)

```python
# backend/tests/integration/test_students.py
import pytest
from fastapi import status


class TestStudentEndpoints:
    """Integration tests for student CRUD endpoints."""

    @pytest.fixture
    def create_student_payload(self):
        """Sample student creation payload."""
        return {
            "first_name": "John",
            "last_name": "Doe",
            "email": "[email protected]",
            "date_of_birth": "2008-05-15T00:00:00Z",
            "grade_level": 9,
        }

    def test_create_student_success(self, client, auth_headers, create_student_payload):
        """Test successful student creation."""
        response = client.post(
            "/api/v1/students/",
            json=create_student_payload,
            headers=auth_headers,
        )
        assert response.status_code == status.HTTP_201_CREATED
        data = response.json()
        assert data["first_name"] == "John"
        assert data["last_name"] == "Doe"
        assert "id" in data
        assert data["is_active"] is True

    def test_create_student_unauthorized(self, client, create_student_payload):
        """Test that unauthenticated requests are rejected."""
        response = client.post(
            "/api/v1/students/",
            json=create_student_payload,
        )
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_create_student_invalid_email(self, client, auth_headers, create_student_payload):
        """Test validation error for invalid email."""
        payload = {**create_student_payload, "email": "invalid-email"}
        response = client.post(
            "/api/v1/students/",
            json=payload,
            headers=auth_headers,
        )
        assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

    def test_create_student_missing_required_field(self, client, auth_headers):
        """Test validation error for missing required field."""
        payload = {
            "first_name": "John",
            # Missing last_name, email, date_of_birth, grade_level
        }
        response = client.post(
            "/api/v1/students/",
            json=payload,
            headers=auth_headers,
        )
        assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

    def test_get_student_success(self, client, auth_headers, create_student_payload):
        """

Related in Web Dev