- 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.
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
- Requirements
- How It Works
- Test Organization
- Writing E2E Tests
- Running Tests
- Troubleshooting
- CI/CD Integration
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 managementschemathesis>=3.30.0- OpenAPI contract testing
How It Works
Testcontainers
Testcontainers automatically manages Docker containers for tests:
- Session-scoped container: A single PostgreSQL 17 container starts once per test session
- Function-scoped isolation: Each test gets fresh tables (drop + recreate)
- 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:
- Schema loading: Reads
/api/v1/openapi.jsonfrom your FastAPI app - Test generation: Creates test cases for each endpoint
- 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:
- Increase Docker resources (memory, CPU)
- Pull the image manually:
docker pull postgres:17-alpine - 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:
- Stop other PostgreSQL containers:
docker stop $(docker ps -q) - 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:
- Rename to
backend-e2e-tests.yml - Push to repository
The workflow:
- Runs on pushes to
main/developaffectingbackend/ - 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_clientfixture 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=Trueenvironment variable - Run E2E tests without Docker