Практикум: тесты миграций Alembic¶
В практикуме 20 тестов, одна исправная история миграций и 14 вариантов с ошибками для магазина с пользователями и заказами. В каждом сломанном варианте изменён один файл ревизии; ошибка описана в BUG.txt.
Нужны Docker и uv. Выполните команды из каталога практикума:
uv run --no-project --python 3.13 --with-requirements requirements.txt python -m pytest -q
uv run --no-project --python 3.13 --with-requirements requirements.txt python run_matrix.py
Первая команда должна показать 20 passed. Скрипт run_matrix.py перебирает все истории и требует, чтобы каждую намеренную ошибку обнаружил нужный тест. Неожиданный успех, пропущенные тесты, ошибки инфраструктуры или ошибка в исправной истории приводят к неуспешному завершению скрипта. Для two_heads запускаются две проверки head и обычный upgrade: остальным тестам нужна однозначная последняя ревизия. Для всех других историй выполняется полный набор.
Чтобы проверить только новые случаи с поведением CHECK и потерей данных вместе с исправным вариантом:
uv run --no-project --python 3.13 --with-requirements requirements.txt python run_matrix.py --variants clean,drift_check_expression,data_loss
Используется образ PostgreSQL postgres:17-alpine. Обычный запуск pytest поднимает один контейнер на тестовую сессию; скрипт перебора вариантов поднимает один контейнер на все дочерние процессы. Каждый тест работает в собственной схеме, которая удаляется после проверки. Контейнер удаляется при выходе. Через MIGRATION_TEST_URL можно передать существующую временную тестовую БД. Переменная VARIANT выбирает историю; по умолчанию это clean.
В requirements.txt закреплены alembic-gauntlet 0.3.0, Alembic 1.20.0, SQLAlchemy 2.0.54, asyncpg 0.31.0, pytest 9.1.1, pytest-asyncio 1.4.0 и testcontainers 4.15.0. В pytest.ini включён asyncio auto mode. После установки alembic-gauntlet его плагин pytest регистрирует фикстуру migration_engine.
| Файл | Назначение |
|---|---|
shop/models.py |
Пользователи, заказы, значения enum и соглашение об именовании |
tests/conftest.py |
Фикстура контейнера/БД и выбор истории миграций |
migrations/env.py |
Принимает соединение и схему от теста; при самостоятельном запуске создаёт engine и фиксирует транзакцию миграций |
tests/test_plain_ci.py |
Один раз применяет head к пустой схеме |
tests/test_by_hand.py |
Пять проверок через API Alembic, включая повторное применение каждой ревизии |
tests/test_gauntlet.py |
Семь унаследованных проверок с включённым сравнением серверных значений по умолчанию |
tests/test_drift_extras.py |
Ручные проверки default, имён CHECK и значений enum |
tests/test_business_rules.py |
Запрещает суммы 0 и -1; проверяет сохранность оплаченного заказа и его пользователя при переходе с 0003 на 0004 |
tests/test_environment.py |
Запускает самостоятельный путь env.py в отдельном процессе; новым соединением проверяет сохранённую ревизию и таблицы |
ci-example.yml |
Шаблон GitHub Actions; для подключения скопируйте в .github/workflows/ и укажите каталог тестов своего проекта |
Исправная история создаёт пользователей, затем заказы и их enum, добавляет CHECK положительной суммы, а после этого users.is_active с серверным default true.
| Вариант | Какая ошибка обнаруживается |
|---|---|
enum_leftover |
Повторное применение после отката падает: enum остался в БД |
wrong_name_in_downgrade |
Откат пытается удалить несуществующий CHECK |
head_noop_downgrade |
Откат последней ревизии ничего не делает; падает только ручной проход вверх-вниз-вверх |
two_heads |
Обе проверки head и upgrade head завершаются ошибкой |
bad_name |
Имя нарушает соглашение об именовании |
drift, drift_type, drift_server_default |
Сравнение схемы находит неверный nullable, тип или default |
drift_index_missing, drift_extra_column |
В схеме нет индекса или есть лишняя колонка |
drift_check_missing |
Явная проверка имён CHECK находит отсутствующее ограничение |
drift_enum_value |
Значения enum расходятся |
drift_check_expression |
Имена совпадают, но проверка нулевой суммы падает |
data_loss |
Схема совпадает, но сохранённый заказ исчезает |
Для head_noop_downgrade, drift_check_expression и data_loss скрипт также проверяет, что все семь унаследованных тестов gauntlet проходят: такие ошибки ловят только ручные тесты. Проверка шагов в gauntlet 0.3.0 ни разу не применяет последнюю ревизию повторно, поэтому сломанный откат этой ревизии находит только ручной проход.
В Alembic 1.20 есть необязательный плагин autogenerate для имён CHECK; здесь он выключен. Ни такое сравнение имён, ни аналогичная проверка gauntlet не доказывают равнозначность условий. Практикум проверяет корректность на небольшом наборе данных, не измеряя длительность блокировок и скорость миграции рабочей БД.