Skip to content

Commit e3bbdd7

Browse files
committed
test: check migrations with pytest-alembic instead of a hand-written drift test
1 parent 6e368ae commit e3bbdd7

9 files changed

Lines changed: 64 additions & 43 deletions

File tree

‎.github/workflows/main.yml‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ jobs:
3030

3131
pytest:
3232
runs-on: ubuntu-latest
33+
name: pytest (${{ matrix.name }})
34+
strategy:
35+
fail-fast: false
36+
matrix:
37+
include:
38+
- name: app
39+
run: |
40+
uv run alembic upgrade head
41+
uv run pytest . --cov=. --cov-report xml
42+
# pytest-alembic upgrades an empty database itself, so no `alembic upgrade` first.
43+
- name: migrations
44+
run: uv run pytest tests/migrations --override-ini=addopts=
3345
services:
3446
postgres:
3547
image: postgres:latest
@@ -52,8 +64,7 @@ jobs:
5264
- run: uv python pin 3.14
5365
- run: |
5466
uv sync --all-extras --all-groups --frozen --no-install-project
55-
uv run alembic upgrade head
56-
uv run pytest . --cov=. --cov-report xml
67+
${{ matrix.run }}
5768
env:
5869
SERVICE_ENVIRONMENT: ci
5970
PYTHONDONTWRITEBYTECODE: 1

‎AGENTS.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ The development workflow runs inside Docker via `just` (`just --list` / read the
1313
Things the recipe names don't make obvious:
1414

1515
- `just test [args]` recreates a clean DB first (`alembic downgrade base` → `upgrade head`) and tears the stack down afterward; `[args]` are passed through to `pytest`.
16+
- `just test-migrations [args]` runs the `pytest-alembic` suite in `tests/migrations/` against an empty database: single head, upgrade, up/down consistency for every revision, and models matching the migrated schema (`alembic check` as a test). `just test` skips that directory (`--ignore` in `addopts`, `omit` in coverage) because it migrates the schema under the transaction-rollback fixture, so the recipe clears `addopts`.
1617
- `just run` applies migrations then starts the app on :8000.
1718
- `just lint` auto-fixes (eof-fixer + `ruff format` + `ruff check --fix` + `ty check`); CI (below) is the no-fix gate.
1819

@@ -22,7 +23,7 @@ Running a single test (inside container or with DB available):
2223
uv run pytest tests/test_decks.py::test_get_decks -x
2324
```
2425

25-
CI (`.github/workflows/main.yml`) runs `ruff format --check`, `ruff check --no-fix`, `ty check`, then `alembic upgrade head` and `pytest` against a Postgres service. Match this locally before pushing.
26+
CI (`.github/workflows/main.yml`) runs `ruff format --check`, `ruff check --no-fix`, `ty check`, then two Postgres jobs: `alembic upgrade head` followed by `pytest`, and the `pytest-alembic` suite. Match this locally before pushing.
2627

2728
## Architecture
2829

‎justfile‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ sh:
99
test *args: down && down
1010
docker compose run application sh -c "sleep 1 && uv run alembic downgrade base && uv run alembic upgrade head && uv run pytest {{ args }}"
1111

12+
test-migrations *args: down && down
13+
docker compose run application sh -c "sleep 1 && uv run pytest tests/migrations --override-ini=addopts= {{ args }}"
14+
1215
run:
1316
docker compose run --service-ports application sh -c "sleep 1 && uv run alembic upgrade head && uv run python -m app"
1417

‎pyproject.toml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dev = [
3333
"pytest-cov",
3434
"pytest-asyncio",
3535
"asgi_lifespan",
36+
"pytest-alembic",
3637
]
3738
lint = [
3839
"ruff",
@@ -79,7 +80,7 @@ isort.no-lines-before = ["standard-library", "local-folder"]
7980
]
8081

8182
[tool.pytest.ini_options]
82-
addopts = "--cov=. --cov-report term-missing --cov-fail-under=100"
83+
addopts = "--cov=. --cov-report term-missing --cov-fail-under=100 --ignore=tests/migrations"
8384
asyncio_mode = "auto"
8485
asyncio_default_fixture_loop_scope = "function"
8586

@@ -90,3 +91,4 @@ exclude_also = [
9091

9192
[tool.coverage.run]
9293
concurrency = ["thread", "greenlet"]
94+
omit = ["tests/migrations/*"]

‎tests/migrations/__init__.py‎

Whitespace-only changes.

‎tests/migrations/conftest.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import typing
2+
3+
import pytest
4+
from sqlalchemy import Engine, create_engine
5+
6+
from app.settings import settings
7+
8+
9+
@pytest.fixture
10+
def alembic_engine() -> typing.Iterator[Engine]:
11+
# Replaces pytest-alembic's default in-memory SQLite engine.
12+
engine: typing.Final = create_engine(settings.db_dsn_parsed.set(drivername="postgresql+psycopg"))
13+
yield engine
14+
engine.dispose()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from pytest_alembic.tests import (
2+
test_model_definitions_match_ddl,
3+
test_single_head_revision,
4+
test_up_down_consistency,
5+
test_upgrade,
6+
)
7+
8+
9+
_ = test_single_head_revision
10+
_ = test_upgrade
11+
_ = test_model_definitions_match_ddl
12+
_ = test_up_down_consistency

‎tests/test_migrations.py‎

Lines changed: 0 additions & 38 deletions
This file was deleted.

‎uv.lock‎

Lines changed: 17 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)