Configuration Management
Environment Config
Tests run against multiple environments: dev, staging, production. Config must be environment-aware without code changes between runs.
Config Model
from pydantic import BaseModel, field_validator
from pydantic_settings import BaseSettings
class EnvConfig(BaseSettings):
base_url: str
api_token: str
db_host: str = "localhost"
db_port: int = 5432
db_name: str = "testdb"
timeout_seconds: int = 30
environment: str = "dev"
model_config = {"env_file": ".env", "env_prefix": "TEST_"}
@field_validator("base_url")
@classmethod
def validate_url(cls, v: str) -> str:
if not v.startswith(("http://", "https://")):
raise ValueError(f"base_url must start with http:// or https://, got: {v}")
return v.rstrip("/")
YAML per Environment
# config/environments/staging.yaml
base_url: "https://api.staging.example.com"
timeout_seconds: 30
db_host: "${DB_HOST}"
db_name: "staging_testdb"
environment: "staging"
import os
import yaml
import re
def load_env_config(env: str = "dev") -> EnvConfig:
config_path = f"config/environments/{env}.yaml"
with open(config_path) as f:
raw = f.read()
# Expand ${VAR} references
expanded = re.sub(
r"\$\{(\w+)\}",
lambda m: os.environ.get(m.group(1), m.group(0)),
raw,
)
data = yaml.safe_load(expanded)
return EnvConfig(**data)
# conftest.py
import pytest
import os
@pytest.fixture(scope="session")
def env_config() -> EnvConfig:
env = os.environ.get("TEST_ENV", "dev")
return load_env_config(env)
Run against staging:
TEST_ENV=staging uv run pytest -m smoke
Secrets Management
Never commit secrets. Not in YAML, not in .env, not in conftest.
| Secret | Storage |
|---|---|
| API tokens | CI secrets (GitHub Secrets, Vault) |
| DB passwords | Environment variables only |
| OAuth credentials | CI environment, .env.local (gitignored) |
| TLS certificates | Secret store, mounted in CI |
# .gitignore
.env
.env.local
config/secrets/
# Fail fast if required secret is missing
import os
def require_env(key: str) -> str:
value = os.environ.get(key)
if not value:
raise EnvironmentError(
f"Required environment variable '{key}' is not set. "
f"Check your .env file or CI secrets configuration."
)
return value
Feature Flags
Test behaviour changes when feature flags are on or off. Tests must be explicit about the flag state they require.
import pytest
@pytest.fixture
def feature_flags(env_config) -> dict:
return {
"new_checkout_flow": env_config.environment != "prod",
"experimental_search": False,
}
@pytest.mark.skipif(
condition=True,
reason="new_checkout_flow flag is disabled in this environment",
)
def test_new_checkout_flow_completes(page, feature_flags):
if not feature_flags["new_checkout_flow"]:
pytest.skip("new_checkout_flow disabled")
...
Config Validation at Startup
Validate config before any test runs. Fail immediately with a clear message.
# conftest.py
def pytest_sessionstart(session) -> None:
import logging
log = logging.getLogger("conftest")
try:
config = load_env_config(os.environ.get("TEST_ENV", "dev"))
log.info("Config loaded: env=%s base_url=%s", config.environment, config.base_url)
except Exception as exc:
raise SystemExit(f"Framework config error: {exc}") from exc
A misconfigured environment produces one clear error, not 500 mysterious failures.