| name | backend-api-test |
| description | Guidelines for writing backend API tests using pytest and FastAPI TestClient. Use this skill when writing or modifying tests in tests/backend directory. |
Backend API Testing Guidelines
This skill provides comprehensive guidelines for writing backend API tests for the Factory Inventory Management System. Follow these patterns to ensure consistent, thorough test coverage.
Directory Structure
All backend tests must be placed in tests/backend/:
tests/backend/
├── conftest.py # Shared fixtures and test client setup
├── test_inventory.py # Inventory endpoint tests
├── test_orders.py # Orders endpoint tests
├── test_dashboard.py # Dashboard endpoint tests
└── test_misc_endpoints.py # Other endpoint tests
File Organization
1. File Naming
- Use
test_<feature>.py format (e.g., test_inventory.py, test_orders.py)
- Group related endpoints in the same file
- Create new files for distinct API feature areas
2. Test Class Structure
Organize tests within a class using descriptive names:
"""
Tests for <feature> API endpoints.
"""
import pytest
class Test<Feature>Endpoints:
"""Test suite for <feature>-related endpoints."""
def test_get_all_<resources>(self, client):
"""Test getting all <resources>."""
Core Testing Patterns
1. Basic Endpoint Tests
Always test the happy path first:
def test_get_all_orders(self, client):
"""Test getting all orders."""
response = client.get("/api/orders")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) > 0
first_order = data[0]
assert "id" in first_order
assert "order_number" in first_order
2. Filter Testing
Test each query parameter filter individually and in combination:
def test_get_orders_by_warehouse(self, client):
"""Test filtering orders by warehouse."""
response = client.get("/api/orders?warehouse=Tokyo")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
for order in data:
assert order["warehouse"] == "Tokyo"
def test_get_orders_multiple_filters(self, client):
"""Test filtering orders with multiple filters."""
response = client.get(
"/api/orders?warehouse=San Francisco&category=Power Supplies&status=Delivered"
)
assert response.status_code == 200
data = response.json()
for order in data:
assert order["warehouse"] == "San Francisco"
assert order["category"].lower() == "power supplies"
assert order["status"].lower() == "delivered"
Common filters to test:
warehouse - Filter by warehouse location
category - Filter by product category
status - Filter by order status (orders only)
month - Filter by month in format YYYY-MM or quarter Q1-2025
3. Single Resource Tests
Test fetching individual resources by ID:
def test_get_order_by_id(self, client):
"""Test getting a specific order by ID."""
response = client.get("/api/orders")
all_orders = response.json()
assert len(all_orders) > 0
first_order_id = all_orders[0]["id"]
response = client.get(f"/api/orders/{first_order_id}")
assert response.status_code == 200
order = response.json()
assert order["id"] == first_order_id
def test_get_nonexistent_order(self, client):
"""Test getting an order that doesn't exist."""
response = client.get("/api/orders/nonexistent-order-999")
assert response.status_code == 404
data = response.json()
assert "detail" in data
assert "not found" in data["detail"].lower()
4. Data Structure Validation
Verify the response structure matches the API contract:
def test_order_items_structure(self, client):
"""Test that order items have proper structure."""
response = client.get("/api/orders")
data = response.json()
for order in data:
assert "items" in order
assert isinstance(order["items"], list)
for item in order["items"]:
assert "sku" in item
assert "name" in item
assert "quantity" in item
assert "unit_price" in item
assert isinstance(item["quantity"], int)
assert isinstance(item["unit_price"], (int, float))
5. Data Type Validation
Ensure numeric fields have correct types and valid ranges:
def test_inventory_quantity_types(self, client):
"""Test that quantity fields are proper numeric types."""
response = client.get("/api/inventory")
data = response.json()
for item in data:
assert isinstance(item["quantity_on_hand"], int)
assert isinstance(item["reorder_point"], int)
assert isinstance(item["unit_cost"], (int, float))
assert item["quantity_on_hand"] >= 0
assert item["reorder_point"] >= 0
assert item["unit_cost"] >= 0
6. Business Logic Validation
Test calculated values and business rules:
def test_order_total_value_calculation(self, client):
"""Test that order total values are reasonable."""
response = client.get("/api/orders")
data = response.json()
for order in data:
assert "total_value" in order
assert isinstance(order["total_value"], (int, float))
assert order["total_value"] > 0
calculated_total = sum(
item["quantity"] * item["unit_price"]
for item in order["items"]
)
assert abs(order["total_value"] - calculated_total) < 0.01
7. Enum/Status Value Validation
Verify constrained fields have valid values:
def test_order_status_values(self, client):
"""Test that orders have valid status values."""
response = client.get("/api/orders")
data = response.json()
valid_statuses = ["delivered", "shipped", "processing", "backordered"]
for order in data:
assert order["status"].lower() in valid_statuses
8. Date Format Validation
Verify date fields are properly formatted:
def test_order_dates_format(self, client):
"""Test that order dates are in proper format."""
response = client.get("/api/orders")
data = response.json()
for order in data:
assert "order_date" in order
assert "expected_delivery" in order
assert "2025-" in order["order_date"]
assert "-" in order["expected_delivery"]
assert "T" in order["expected_delivery"]
9. Cross-Endpoint Validation
Test that aggregated endpoints match raw data:
def test_dashboard_pending_orders_calculation(self, client):
"""Test that pending orders are calculated correctly."""
orders_response = client.get("/api/orders")
all_orders = orders_response.json()
pending_count = sum(
1 for order in all_orders
if order["status"].lower() in ["processing", "backordered"]
)
dashboard_response = client.get("/api/dashboard/summary")
dashboard_data = dashboard_response.json()
assert dashboard_data["pending_orders"] == pending_count
Using Fixtures
Available Fixtures (from conftest.py)
client - FastAPI TestClient instance (required for all tests)
sample_inventory_item - Example inventory item structure
sample_order - Example order structure
Using the Client Fixture
def test_example(self, client):
"""Every test method needs the client fixture."""
response = client.get("/api/endpoint")
assert response.status_code == 200
Creating New Fixtures
Add shared fixtures to conftest.py:
@pytest.fixture
def sample_warehouse_data():
"""Sample warehouse data for testing."""
return {
"name": "San Francisco",
"location": "CA",
}
Test Naming Conventions
Use descriptive names that clearly indicate what is being tested:
test_get_all_<resources> - Get all items without filters
test_get_<resource>_by_<filter> - Single filter tests
test_get_<resource>_multiple_filters - Combined filter tests
test_get_<resource>_by_id - Single item retrieval
test_get_nonexistent_<resource> - 404 handling
test_<resource>_<field>_structure - Data structure validation
test_<resource>_<field>_types - Data type validation
test_<resource>_<calculation>_calculation - Business logic
Common Assertions
Status Codes
assert response.status_code == 200
assert response.status_code == 404
assert response.status_code == 422
Response Types
data = response.json()
assert isinstance(data, list)
assert isinstance(data, dict)
assert len(data) > 0
Field Presence
assert "field_name" in data
assert "detail" in error_response
String Comparisons (Case-Insensitive)
assert order["status"].lower() == "delivered"
assert item["category"].lower() == "power supplies"
Floating Point Comparisons
assert abs(calculated - expected) < 0.01
API Endpoint Reference
Inventory Endpoints
GET /api/inventory - All inventory items
- Filters:
warehouse, category
GET /api/inventory/{id} - Single inventory item
Orders Endpoints
GET /api/orders - All orders
- Filters:
warehouse, category, status, month
GET /api/orders/{id} - Single order
Dashboard Endpoints
GET /api/dashboard/summary - Dashboard summary
- Filters:
warehouse, category, status, month
Other Endpoints
GET /api/demand - Demand forecast (no filters)
GET /api/backlog - Backlog items (no filters)
GET /api/spending/* - Spending data endpoints
Common Values for Testing
Warehouses
- San Francisco
- London
- Tokyo
Categories
- Circuit Boards
- Sensors
- Power Supplies
- Actuators
- Controllers
Order Statuses
- Delivered
- Shipped
- Processing
- Backordered
Date Formats
- Single month:
2025-01, 2025-02, etc.
- Quarter:
Q1-2025, Q2-2025, etc.
- All:
all
Best Practices
- Test one thing per test - Each test should verify a single behavior
- Use descriptive assertions - Clear error messages help debugging
- Test edge cases - Empty results, nonexistent IDs, invalid filters
- Verify data integrity - Check types, ranges, and calculated values
- Test filters independently - Then test combinations
- Use case-insensitive comparisons - For string fields like status/category
- Allow floating point tolerance - Use
abs(a - b) < 0.01 for money calculations
- Test error responses - Verify proper 404 handling
- Validate complete structure - Check all required fields are present
- Cross-validate endpoints - Dashboard should match raw data
Running Tests
Run from the repository root.
uv run --project server pytest tests/backend/
uv run --project server pytest tests/backend/test_orders.py
uv run --project server pytest tests/backend/test_orders.py::TestOrdersEndpoints
uv run --project server pytest tests/backend/test_orders.py::TestOrdersEndpoints::test_get_all_orders
uv run --project server pytest tests/backend/ -v
uv run --project server pytest tests/backend/ --cov=server
Example: Complete Test File Template
"""
Tests for <feature> API endpoints.
"""
import pytest
class Test<Feature>Endpoints:
"""Test suite for <feature>-related endpoints."""
def test_get_all_<resources>(self, client):
"""Test getting all <resources>."""
response = client.get("/api/<resources>")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) > 0
first_item = data[0]
assert "id" in first_item
def test_get_<resource>_by_filter(self, client):
"""Test filtering <resources> by <filter>."""
response = client.get("/api/<resources>?<filter>=<value>")
assert response.status_code == 200
data = response.json()
for item in data:
assert item["<filter>"] == "<value>"
def test_get_<resource>_by_id(self, client):
"""Test getting a specific <resource> by ID."""
response = client.get("/api/<resources>")
all_items = response.json()
item_id = all_items[0]["id"]
response = client.get(f"/api/<resources>/{item_id}")
assert response.status_code == 200
item = response.json()
assert item["id"] == item_id
def test_get_nonexistent_<resource>(self, client):
"""Test getting a <resource> that doesn't exist."""
response = client.get("/api/<resources>/nonexistent-999")
assert response.status_code == 404
data = response.json()
assert "detail" in data
assert "not found" in data["detail"].lower()
Key Reminders
- Always use the
client fixture - It's the TestClient for making API calls
- Test filters thoroughly - Individual filters and combinations
- Validate response structure - Check all required fields exist
- Use lowercase for string comparisons - Categories and statuses vary in case
- Test both success and error paths - 200 and 404 responses
- Verify data types - Use
isinstance() for proper type checking
- Test business logic - Calculations, aggregations, and derived values
- Keep tests independent - Each test should work in isolation