Перейти к содержанию

Практикум: тесты миграций 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 не доказывают равнозначность условий. Практикум проверяет корректность на небольшом наборе данных, не измеряя длительность блокировок и скорость миграции рабочей БД.

Читать статью.

Исходный код практикума на GitHub.