Testing with pytest

This project uses pytest for testing, along with pytest-asyncio for testing asynchronous FastAPI endpoints.

Test Structure

The test suite is organized in the tests/ directory with the following structure:

tests/
├── __init__.py
├── conftest.py          # Test configuration and fixtures
└── test_health.py       # Health endpoint tests

Test Configuration

The testing setup is configured in tests/conftest.py, which provides:

  • Async test client fixture - Creates an HTTPX AsyncClient for testing FastAPI endpoints

  • Automatic cleanup - Clears dependency overrides after each test

Key Components:

from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlmodel import SQLModel

from app.main import app
from app.db.database import get_session


Running Tests

Local Testing with Pipenv

For local development setup:

# Activate pipenv environment
pipenv shell

# Run all tests
pipenv run pytest

# Run tests with verbose output
pipenv run pytest -v

# Run tests in a specific file
pipenv run pytest tests/test_health.py

# Run a specific test function
pipenv run pytest tests/test_health.py::TestHealthEndpoint::test_health_endpoint_returns_200

Test Output and Coverage

Docker testing with coverage:

# Run tests with coverage (generates HTML, XML, and terminal reports)
make test-cov

# View coverage report in browser (after running test-cov)
# Open htmlcov/index.html in your browser

Local testing with additional options:

# Show print statements during tests
pipenv run pytest -s

# Show detailed test results
pipenv run pytest -v

# Stop after first failure
pipenv run pytest -x

# Run tests matching a pattern
pipenv run pytest -k "health"

Async Testing

This project uses pytest-asyncio to handle asynchronous tests. All async test functions should be marked with @pytest.mark.asyncio:

@pytest.mark.asyncio
async def test_async_endpoint(self, client: AsyncClient):
    """Test an async endpoint."""
    response = await client.get("/some-endpoint")
    assert response.status_code == 200

Writing Tests

Test Class Organization

Tests are organized into classes that group related functionality:

    """Test cases for the health endpoint."""

    def test_health_endpoint_returns_200(self, client: TestClient):
        """Test that the health endpoint returns a 200 status code."""
        response = client.get("/health")
        assert response.status_code == 200

Using the Test Client

The client fixture provides an HTTPX AsyncClient configured for testing:

@pytest.mark.asyncio
async def test_api_endpoint(self, client: AsyncClient):
    # GET request
    response = await client.get("/api/endpoint")

    # POST request with JSON data
    response = await client.post("/api/create", json={"name": "test"})

    # Request with headers
    response = await client.get("/api/protected", headers={"Authorization": "Bearer token"})

    # Assertions
    assert response.status_code == 200
    assert response.json() == {"expected": "result"}

Test Examples

Here are common testing patterns for this FastAPI project:

Testing JSON responses:

@pytest.mark.asyncio
async def test_json_response(self, client: AsyncClient):
    response = await client.get("/api/data")
    assert response.status_code == 200
    data = response.json()
    assert "key" in data
    assert data["key"] == "expected_value"

Testing error responses:

@pytest.mark.asyncio
async def test_not_found(self, client: AsyncClient):
    response = await client.get("/api/nonexistent")
    assert response.status_code == 404

Testing POST endpoints:

@pytest.mark.asyncio
async def test_create_resource(self, client: AsyncClient):
    payload = {"name": "test", "description": "Test resource"}
    response = await client.post("/api/resources", json=payload)
    assert response.status_code == 201
    created = response.json()
    assert created["name"] == payload["name"]

Best Practices

Test Organization

  • Group related tests into classes with descriptive names

  • Use descriptive test method names that explain what is being tested

  • Add docstrings to test classes and methods for documentation

Test Data

  • Use fixtures for reusable test data and setup

  • Keep test data minimal - only include what’s necessary for the test

  • Avoid hardcoded values where possible - use variables or fixtures

Assertions

  • Test one thing per test - each test should verify a single behavior

  • Use specific assertions - check exact values rather than just truthy/falsy

  • Test both success and failure cases for each endpoint

Environment Isolation

  • Tests run in isolation - the test client and fixtures ensure clean state

  • Dependencies are automatically cleared after each test via conftest.py

  • Use dependency overrides for mocking external services if needed

Common Test Commands Summary

Local Commands (Pipenv)

# Basic testing
pipenv run pytest                    # Run all tests
pipenv run pytest -v                # Verbose output
pipenv run pytest -s                # Show print statements
pipenv run pytest -x                # Stop on first failure

# Specific test selection
pipenv run pytest tests/test_health.py                      # Run specific file
pipenv run pytest -k "health"                               # Run tests matching pattern
pipenv run pytest tests/test_health.py::TestHealthEndpoint  # Run specific class

# Output and reporting
pipenv run pytest --tb=short        # Shorter traceback format
pipenv run pytest --tb=no           # No traceback
pipenv run pytest -q                # Quiet output