Getting Started
This guide walks you through setting up pytest-mrt from scratch, even if you've never written a pytest plugin before.
What you need
- Python 3.10 or higher
- An existing project that uses Alembic for database migrations
- A test database (separate from your production DB — see below)
Step 1 — Install
That's it. Because pytest-mrt is a pytest plugin, it automatically activates when you run pytest. You don't need to import it anywhere.
Step 2 — Create a test database
pytest-mrt will run real migrations against a real database. Never use your production database.
Step 3 — Create conftest.py
conftest.py is a special pytest file where you configure plugins. Create it in your project root (or test directory):
# conftest.py
import os
from pytest_mrt import MRTConfig
def pytest_configure(config):
config._mrt_config = MRTConfig(
alembic_ini="alembic.ini", # path to your alembic.ini file
db_url="postgresql://localhost/myapp_test", # your test database URL
)
Use environment variables in CI
This way your local machine uses SQLite and CI uses PostgreSQL — no config changes needed.Step 4 — Write your first test
Create a test file:
# tests/test_migrations.py
def test_all_migrations_are_reversible(mrt):
"""
Checks every migration in your project.
Seeds real data, runs upgrade, runs downgrade,
and verifies nothing was lost.
"""
mrt.assert_all_reversible()
What is mrt?
mrt is a pytest fixture — a special argument that pytest automatically injects into your test function. You don't need to import or create it. Just add mrt as a parameter and it works.
Step 5 — Run it
The -s flag is important — it lets the output render properly.
If all migrations are safe:
──────────── MRT — Migration Rollback Test ────────────
✓ 001 reversible
✓ 002 reversible
✓ 003 reversible
╭──────────────────────────────────────────╮
│ All 3 migration(s) are safely reversible│
╰──────────────────────────────────────────╯
PASSED
If a migration is unsafe:
✓ 001 reversible
✓ 002 reversible
✗ 003 data loss detected
└─ Table 'users': 3/3 rows lost after rollback
╭──────────────────────────────────────────────────╮
│ 1 migration(s) will cause data loss on rollback │
╰──────────────────────────────────────────────────╯
FAILED
Testing only new migrations (recommended for CI)
Running all migrations on every PR can be slow. In CI, you usually only need to check the migration added in the current PR.
# tests/test_migrations.py
def test_this_migration(mrt):
"""
Replace 'abc1234' with the revision ID of your new migration.
Find it at the top of your migration file:
revision = 'abc1234'
"""
result = mrt.check_revision("abc1234")
assert result.passed, result.failure_summary()
If the migration is unsafe, result.failure_summary() prints exactly what went wrong:
Static analysis — catch problems without running anything
Before touching your database, scan migration files for known dangerous patterns:
This is fast (no DB needed) and catches things like:
op.drop_column()in upgrade — data loss even if downgrade re-adds the columndef downgrade(): pass— rollback silently does nothingALTER TYPE ADD VALUE— can't be rolled back in PostgreSQL- ...and 40+ more patterns
Exit codes:
| Code | Meaning |
|---|---|
0 |
Clean — no issues |
1 |
Warnings found (without --strict) |
2 |
Errors found, or warnings with --strict |
Add --strict to also exit 2 on warnings:
Built-in default tests
When MRTConfig is configured, pytest-mrt automatically injects 6 safety tests into your suite — no extra test files needed:
| Test | What it checks |
|---|---|
test_mrt_single_head |
Migration history has exactly one head |
test_mrt_upgrade |
alembic upgrade head completes without error |
test_mrt_downgrade_base |
Full up → down → up cycle succeeds |
test_mrt_up_down_consistency |
Every migration is safely reversible |
test_mrt_static_no_errors |
Zero static analysis errors |
test_mrt_schema_matches_models |
DB schema matches ORM models (requires target_metadata) |
To opt out of a specific test:
Suppressing known warnings
Use # noqa: MRTxxx to suppress a specific warning on a line (ruff/flake8 convention):
Use bare # noqa to suppress all MRT warnings on a line.
Adding to GitHub Actions
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install pytest-mrt psycopg2-binary alembic
- name: Static migration check (fast, no DB)
run: mrt check migrations/versions/
- name: Dynamic migration check (with real DB)
env:
TEST_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
run: pytest tests/test_migrations.py -v -s
Configuration options
All options for MRTConfig:
from pytest_mrt import MRTConfig
MRTConfig(
# Alembic
alembic_ini="alembic.ini", # Path to alembic.ini (default: "alembic.ini")
db_url="postgresql://...", # Test DB URL. Never use production.
seed_rows=3, # Rows inserted per table before each rollback check
# Django (use instead of alembic_ini)
django_settings="myapp.settings", # Django settings module
django_apps=["users", "orders"], # Limit to specific app labels (None = all)
django_project_dir="/path/to/app", # Project root if conftest is elsewhere
# Rollback floor — skip old migrations already tested historically
minimum_downgrade_revision="abc123def456", # Alembic: revision ID
# minimum_downgrade_revision="myapp.0050_baseline", # Django: app_label.migration_name
# Skip specific migrations
skip={
"abc123": "Intentional data migration. Reviewed 2024-01-15. See ADR-007.",
},
# Override severity for a specific pattern
severity_overrides={"INDEX without CONCURRENTLY": "error"},
# Custom seed data per table
custom_seeds={"users": lambda: [{"id": 1, "name": "Alice"}]},
# Schema drift check
target_metadata="myapp.models:Base",
# Timeout per migration check in seconds
migration_timeout=60,
)
Common errors
MRTConfig not set
You haven't created conftest.py or the pytest_configure function is missing. See Step 3.
could not connect to server
Your test database isn't running, or the URL is wrong. Check:
FAILED — Table not found
The migration file references a table that doesn't exist yet. Make sure your migrations run in the correct order and alembic.ini points to the right versions directory.
Target database is not up to date
Run alembic upgrade head on your test database first, then run the tests again. Or let pytest-mrt handle it — assert_all_reversible() starts from base automatically.