Skip to content

Test Environment Design

Environments

Local

Developer machine or CI agent running the full stack via Docker Compose.

Property Value
Setup docker compose up
Data Ephemeral, reset per run
Speed Fast for unit/integration
Use TDD cycle, rapid feedback

Rules: - Services defined in docker-compose.yml at project root - DB migrations run automatically on container start - No production credentials — always use test secrets

Staging

Shared, persistent environment deployed from main or release branch.

Property Value
Setup Deployed by CI/CD
Data Shared — use isolated test accounts
Speed Real network latency
Use Pre-production smoke tests, E2E

Rules: - Tests must not pollute shared state — always clean up - Use dedicated test tenant / user accounts - Feature flags controlled by environment variable, not code

Production-Like

Ephemeral environment spun up per PR or per test run, identical to production.

Property Value
Setup IaC (Terraform / Pulumi)
Data Seeded snapshot of production-like data
Speed Slow to provision
Use Release validation, security scans

Tools: Testcontainers Cloud, Ephemeral Envs (Railway, Render, Fly.io).


Configuration

Environment Variables

All environment-specific values injected via environment variables.

import os

BASE_URL = os.environ["TEST_BASE_URL"]          # e.g. http://localhost:8000
DB_URL   = os.environ["TEST_DATABASE_URL"]      # e.g. postgresql://...
API_KEY  = os.environ["TEST_API_KEY"]           # test-only key, never prod

Loaded via pytest-dotenv or a .env.test file:

TEST_BASE_URL=http://localhost:8000
TEST_DATABASE_URL=postgresql://test:test@localhost:5432/testdb
TEST_API_KEY=test-api-key-local

.env.test is committed. .env.production is never committed.

Feature Flags

Decouple tests from unreleased features:

import os

def is_feature_enabled(flag: str) -> bool:
    return os.environ.get(f"FEATURE_{flag.upper()}", "false").lower() == "true"

In tests:

@pytest.mark.skipif(
    not is_feature_enabled("NEW_PAYMENTS"),
    reason="NEW_PAYMENTS feature not enabled",
)
def test_new_payment_flow(api_client):
    ...


Dependency Management

External Services

Dependency Local strategy CI strategy
PostgreSQL Docker Compose Testcontainers or service container
Redis Docker Compose Testcontainers
S3-compatible storage MinIO in Docker Localstack / Testcontainers
SMTP Mailhog in Docker Mock / MailSlurp
External HTTP APIs WireMock / httpx mock WireMock in CI container

Testcontainers (Python)

import pytest
from testcontainers.postgres import PostgresContainer

@pytest.fixture(scope="session")
def postgres():
    with PostgresContainer("postgres:16-alpine") as pg:
        yield pg.get_connection_url()

Databases

Rules: - Use a dedicated test database, never the development database - Run migrations in CI before tests: alembic upgrade head - Isolate data per test with transactions or table truncation - Never use DROP TABLE in test teardown — use TRUNCATE ... RESTART IDENTITY CASCADE


Environment Decision Table

Need Local Staging Prod-like
TDD / unit tests Yes No No
API integration tests Yes Yes No
E2E smoke tests Yes (Docker) Yes Yes
Performance tests No Yes Yes
Security scans No No Yes