Files
fast-next-template/backend/docs/E2E_TESTING.md
Felipe Cardoso c0b253a010 Add support for E2E testing infrastructure and OAuth configurations
- Introduced make commands for E2E tests using Testcontainers and Schemathesis.
- Updated `.env.demo` with configurable OAuth settings for Google and GitHub.
- Enhanced `README.md` with updated environment setup instructions.
- Added E2E testing dependencies and markers in `pyproject.toml` for real PostgreSQL and API contract validation.
- Included new libraries (`arrow`, `attrs`, `docker`, etc.) for testing and schema validation workflows.
2025-11-25 22:24:23 +01:00

7.9 KiB

Backend E2E Testing Guide

End-to-end testing infrastructure using Testcontainers (real PostgreSQL) and Schemathesis (OpenAPI contract testing).

Table of Contents


Quick Start

# 1. Install E2E dependencies
make install-e2e

# 2. Ensure Docker is running
make check-docker

# 3. Run E2E tests
make test-e2e

Requirements

Docker

E2E tests use Testcontainers to spin up real PostgreSQL containers. Docker must be running:

  • macOS/Windows: Docker Desktop
  • Linux: Docker Engine (sudo systemctl start docker)

Dependencies

E2E tests require additional packages beyond the standard dev dependencies:

# Install E2E dependencies
make install-e2e

# Or manually:
uv sync --extra dev --extra e2e

This installs:

  • testcontainers[postgres]>=4.0.0 - Docker container management
  • schemathesis>=3.30.0 - OpenAPI contract testing

How It Works

Testcontainers

Testcontainers automatically manages Docker containers for tests:

  1. Session-scoped container: A single PostgreSQL 17 container starts once per test session
  2. Function-scoped isolation: Each test gets fresh tables (drop + recreate)
  3. Automatic cleanup: Container is destroyed when tests complete

This approach catches bugs that SQLite-based tests miss:

  • PostgreSQL-specific SQL behavior
  • Real constraint violations
  • Actual transaction semantics
  • JSONB column behavior

Schemathesis

Schemathesis generates test cases from your OpenAPI schema:

  1. Schema loading: Reads /api/v1/openapi.json from your FastAPI app
  2. Test generation: Creates test cases for each endpoint
  3. Response validation: Verifies responses match documented schema

This catches:

  • Undocumented response codes
  • Schema mismatches (wrong types, missing fields)
  • Edge cases in input validation

Test Organization

backend/tests/
├── e2e/                          # E2E tests (PostgreSQL, Docker required)
│   ├── __init__.py
│   ├── conftest.py               # Testcontainers fixtures
│   ├── test_api_contracts.py     # Schemathesis schema tests
│   └── test_database_workflows.py # PostgreSQL workflow tests
│
├── api/                          # Integration tests (SQLite, fast)
├── crud/                         # Unit tests
└── conftest.py                   # Standard fixtures

Test Markers

Tests use pytest markers for filtering:

Marker Description
@pytest.mark.e2e End-to-end test requiring Docker
@pytest.mark.postgres PostgreSQL-specific test
@pytest.mark.schemathesis Schemathesis schema test

Writing E2E Tests

Basic E2E Test

import pytest
from uuid import uuid4

@pytest.mark.e2e
@pytest.mark.postgres
@pytest.mark.asyncio
async def test_user_workflow(e2e_client):
    """Test user registration with real PostgreSQL."""
    email = f"test-{uuid4().hex[:8]}@example.com"

    response = await e2e_client.post(
        "/api/v1/auth/register",
        json={
            "email": email,
            "password": "SecurePassword123!",
            "first_name": "Test",
            "last_name": "User",
        },
    )

    assert response.status_code in [200, 201]
    assert response.json()["email"] == email

Available Fixtures

Fixture Scope Description
postgres_container session Raw Testcontainers PostgreSQL container
async_postgres_url session Asyncpg-compatible connection URL
e2e_db_session function SQLAlchemy AsyncSession with fresh tables
e2e_client function httpx AsyncClient connected to real DB

Schemathesis Test

import pytest
import schemathesis
from hypothesis import settings, Phase

from app.main import app

schema = schemathesis.from_asgi("/api/v1/openapi.json", app=app)

@pytest.mark.e2e
@pytest.mark.schemathesis
@schema.parametrize(endpoint="/api/v1/auth/register")
@settings(max_examples=20)
def test_registration_schema(case):
    """Test registration endpoint conforms to schema."""
    response = case.call_asgi()
    case.validate_response(response)

Running Tests

Commands

# Run all E2E tests
make test-e2e

# Run only Schemathesis schema tests
make test-e2e-schema

# Run all tests (unit + integration + E2E)
make test-all

# Check Docker availability
make check-docker

Direct pytest

# All E2E tests
IS_TEST=True PYTHONPATH=. uv run pytest tests/e2e/ -v

# Only PostgreSQL tests
IS_TEST=True PYTHONPATH=. uv run pytest tests/e2e/ -v -m postgres

# Only Schemathesis tests
IS_TEST=True PYTHONPATH=. uv run pytest tests/e2e/ -v -m schemathesis

Troubleshooting

Docker Not Running

Error:

Docker is not running!
E2E tests require Docker to be running.

Solution:

# macOS/Windows
# Open Docker Desktop

# Linux
sudo systemctl start docker

Testcontainers Not Installed

Error:

SKIPPED: testcontainers not installed - run: make install-e2e

Solution:

make install-e2e
# Or: uv sync --extra dev --extra e2e

Container Startup Timeout

Error:

testcontainers.core.waiting_utils.UnexpectedResponse

Solutions:

  1. Increase Docker resources (memory, CPU)
  2. Pull the image manually: docker pull postgres:17-alpine
  3. Check Docker daemon logs: docker logs

Port Conflicts

Error:

Error starting container: port is already allocated

Solution: Testcontainers uses random ports, so conflicts are rare. If occurring:

  1. Stop other PostgreSQL containers: docker stop $(docker ps -q)
  2. Check for orphaned containers: docker container prune

Ryuk/Reaper Port 8080 Issues

Error:

ConnectionError: Port mapping for container ... and port 8080 is not available

Solution: This is related to the Testcontainers Reaper (Ryuk) which handles automatic cleanup. The conftest.py automatically disables Ryuk to avoid this issue. If you still encounter this error, ensure you're using the latest conftest.py or set the environment variable:

export TESTCONTAINERS_RYUK_DISABLED=true

Parallel Test Execution Issues

Error:

ScopeMismatch: ... cannot use a higher-scoped fixture 'postgres_container'

Solution: E2E tests must run sequentially (not in parallel) because they share a session-scoped PostgreSQL container. The Makefile commands use -n 0 to disable parallel execution. If running pytest directly, add -n 0:

IS_TEST=True PYTHONPATH=. uv run pytest tests/e2e/ -v -n 0

CI/CD Integration

GitHub Actions

A workflow template is provided at .github/workflows/backend-e2e-tests.yml.template.

To enable:

  1. Rename to backend-e2e-tests.yml
  2. Push to repository

The workflow:

  • Runs on pushes to main/develop affecting backend/
  • Uses continue-on-error: true (E2E failures don't block merge)
  • Caches uv dependencies for speed

Local CI Simulation

# Run what CI runs
make test-all

Best Practices

DO

  • Use unique emails per test: f"test-{uuid4().hex[:8]}@example.com"
  • Mark tests with appropriate markers: @pytest.mark.e2e
  • Keep E2E tests focused on critical workflows
  • Use e2e_client fixture for most tests

DON'T

  • Share state between tests (each test gets fresh tables)
  • Test every endpoint in E2E (use unit tests for edge cases)
  • Skip the IS_TEST=True environment variable
  • Run E2E tests without Docker

Further Reading