Configuration¶
alembic-gauntlet provides several configuration options to customize test behavior.
Required fixtures¶
orm_metadata¶
Purpose: Provides SQLAlchemy metadata for schema consistency checks.
from sqlalchemy import MetaData
import pytest
@pytest.fixture
def orm_metadata(self) -> MetaData:
from myapp.db import Base
return Base.metadata
When to use: Always required for test_migrations_up_to_date and test_naming_conventions.
migration_db_url¶
Purpose: Database connection URL for running migrations.
@pytest.fixture(scope="session")
def migration_db_url(self) -> str:
return "postgresql+asyncpg://user:pass@localhost:5432/testdb"
Scope: Typically session to reuse the database URL across all tests.
Formats:
- postgresql+asyncpg://user:pass@host:port/dbname (recommended)
- postgresql+psycopg://user:pass@host:port/dbname
- postgresql://user:pass@host:port/dbname
Optional fixtures¶
alembic_config¶
Purpose: Customize Alembic configuration.
from alembic.config import Config
@pytest.fixture
def alembic_config(self) -> Config:
config = Config("alembic.ini")
config.set_main_option("script_location", "myapp/migrations")
return config
Default behavior: Automatically discovers alembic.ini in project root.
Use cases: - Non-standard Alembic config location - Dynamic configuration per environment - Multiple migration directories
migration_engine¶
Purpose: Provide custom AsyncEngine for migrations.
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
@pytest.fixture
async def migration_engine(self, migration_db_url: str) -> AsyncEngine:
engine = create_async_engine(
migration_db_url,
poolclass=NullPool,
echo=True, # Log all SQL
)
try:
yield engine
finally:
await engine.dispose()
Use cases: - Custom pool configuration - SQL query logging - Connection middleware
Class attributes¶
migration_diff_ignore_tables¶
Purpose: Ignore specific tables in schema consistency checks.
from typing import ClassVar
class TestMyMigrations(MigrationTestBase):
migration_diff_ignore_tables: ClassVar[list[str]] = ["alembic_version", "events_default"]
Default: ["alembic_version"]
Use cases: - Partitioned tables not managed by Alembic - External tables (e.g., PostGIS extension tables) - Temporary tables
allowed_index_prefixes¶
Purpose: Allowed prefixes for index names.
class TestMyMigrations(MigrationTestBase):
allowed_index_prefixes: ClassVar[list[str]] = ["idx_", "uq_", "ix_"]
Default: ["idx_", "uq_"]
Examples:
- idx_users_email ✅
- uq_users_email ✅
- ix_users_email ✅ (if "ix_" is in the list)
- users_email_idx ❌ (wrong location)
allowed_index_suffixes¶
Purpose: Allowed suffixes for index names.
class TestMyMigrations(MigrationTestBase):
allowed_index_suffixes: ClassVar[list[str]] = ["_idx", "_pkey", "_key", "_uniq"]
Default: ["_idx", "_pkey", "_key"]
Examples:
- users_email_idx ✅
- users_pkey ✅
- users_email_key ✅
- users_email_index ❌ (wrong suffix)
allowed_fk_suffixes¶
Purpose: Allowed suffixes for foreign key names.
class TestMyMigrations(MigrationTestBase):
allowed_fk_suffixes: ClassVar[list[str]] = ["_fkey", "_fk"]
Default: ["_fkey"]
Examples:
- profiles_user_id_fkey ✅
- profiles_user_id_fk ✅ (if "_fk" is in the list)
- profiles_user_id_foreign ❌
Complete example¶
import pytest
from typing import ClassVar
from alembic.config import Config
from alembic_gauntlet import MigrationTestBase
from sqlalchemy import MetaData
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
from sqlalchemy.pool import NullPool
@pytest.mark.integration
class TestMyMigrations(MigrationTestBase):
"""Fully configured migration tests."""
# Customize ignored tables
migration_diff_ignore_tables: ClassVar[list[str]] = [
"alembic_version",
"spatial_ref_sys", # PostGIS table
"events_default", # Partitioned table
]
# Customize naming conventions
allowed_index_prefixes: ClassVar[list[str]] = ["idx_", "uq_", "ix_"]
allowed_index_suffixes: ClassVar[list[str]] = ["_idx", "_pkey", "_key", "_uniq"]
allowed_fk_suffixes: ClassVar[list[str]] = ["_fkey", "_fk"]
@pytest.fixture
def orm_metadata(self) -> MetaData:
from myapp.db import Base
return Base.metadata
@pytest.fixture(scope="session")
def migration_db_url(self) -> str:
# Use environment variable in CI
import os
return os.getenv(
"TEST_DB_URL",
"postgresql+asyncpg://postgres:postgres@localhost:5432/test_db"
)
@pytest.fixture
def alembic_config(self) -> Config:
config = Config("alembic.ini")
# Override script location if needed
config.set_main_option("script_location", "myapp/alembic")
return config
@pytest.fixture
async def migration_engine(self, migration_db_url: str) -> AsyncEngine:
engine = create_async_engine(
migration_db_url,
poolclass=NullPool,
echo=False, # Set to True to debug SQL
)
try:
yield engine
finally:
await engine.dispose()
Environment-specific configuration¶
Development vs CI¶
import os
import pytest
@pytest.fixture(scope="session")
def migration_db_url(self) -> str:
if os.getenv("CI"):
# CI environment (GitHub Actions, GitLab CI, etc.)
return "postgresql+asyncpg://postgres:postgres@postgres:5432/test_db"
else:
# Local development
return "postgresql+asyncpg://postgres:postgres@localhost:5432/test_db"
Multiple databases¶
@pytest.fixture(scope="session", params=["postgresql", "cockroachdb"])
def migration_db_url(self, request) -> str:
db_urls = {
"postgresql": "postgresql+asyncpg://user:pass@localhost:5432/test_db",
"cockroachdb": "cockroachdb+asyncpg://root@localhost:26257/test_db",
}
return db_urls[request.param]
Disabling specific tests¶
Skip naming convention test¶
@pytest.mark.skip(reason="Custom naming conventions not yet enforced")
async def test_naming_conventions(self, *args, **kwargs):
pass
Skip stairway test for specific revision¶
async def test_stairway_upgrade_downgrade(
self,
isolated_migration_schema: str,
migration_engine: AsyncEngine,
alembic_config: Config,
) -> None:
"""Override to skip problematic revisions."""
revisions = await get_all_revisions(alembic_config)
for revision in revisions:
if revision == "abc123": # Skip specific revision
continue
# Run stairway test for this revision
await run_alembic_upgrade(alembic_config, migration_engine, revision)
await run_alembic_downgrade(alembic_config, migration_engine, "-1")
await run_alembic_upgrade(alembic_config, migration_engine, revision)
Pytest markers¶
Custom markers for migration tests¶
# pyproject.toml
[tool.pytest.ini_options]
markers = [
"integration: integration tests requiring database",
"migrations: migration-specific tests",
"slow: slow tests that may take several seconds",
]
@pytest.mark.integration
@pytest.mark.migrations
@pytest.mark.slow
class TestMyMigrations(MigrationTestBase):
...
Run only migration tests:
Parallel execution¶
alembic-gauntlet supports parallel test execution with pytest-xdist:
Each test gets an isolated PostgreSQL schema, so tests don't interfere.
Note: Requires unique schema names per test run (handled automatically).
Logging and debugging¶
Enable SQL logging¶
@pytest.fixture
async def migration_engine(self, migration_db_url: str) -> AsyncEngine:
engine = create_async_engine(
migration_db_url,
poolclass=NullPool,
echo=True, # Log all SQL statements
)
try:
yield engine
finally:
await engine.dispose()
Enable Alembic logging¶
import logging
@pytest.fixture(autouse=True)
def setup_logging(self):
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("alembic").setLevel(logging.DEBUG)
Next steps¶
- Advanced usage — custom validations, mixins
- API reference — complete API docs