Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Тестирование миграций БД и фикстуры данных

Тесты для миграций и данных: как не сломать прод следующей alembic-ревизией

Проверка миграций БД в CI: upgrade/downgrade на реальной базе, тесты данных и фикстуры, factory-подход, тестирование expand/contract и seed-данных.

Прямой ответ

Миграции БД — исполняемый на проде код без тестов в большинстве проектов, и это одна из самых дорогих дыр: сломанная ревизия обнаруживается в момент применения (на живой базе, в окне деплоя). Минимальный контур проверки: CI прогоняет всю цепочку ревизий на реальной БД (testcontainers-Postgres — интеграционные тесты): alembic upgrade head с нуля (fresh-путь), downgrade -1 / upgrade +1 для новой ревизии (обратимость — где заявлена), и upgrade с данными (схема предыдущей версии + наполненные таблицы — миграции с data-преобразованиями ломаются именно на данных, не на пустой схеме). Данные в тестах — factory-подход (factory_boy: объекты создаются кодом с валидными дефолтами и явными переопределениями), не SQL-дампы и не гигантские JSON-фикстуры (дамп не читается, не ревьюится, ломается при каждом изменении схемы; фабрика — живёт и эволюционирует со схемой). Фикстуры pytest по уровням: session — схема/БД, function — транзакционный откат (тесты изолированы, порядок не важен — фикстуры-практика); seed-данные прода — версионируемый код, а не «табличка в вики» (воспроизводимость стендов — 12-factor про окружения).

Тестирование миграций: три обязательных прогона

Прогон 1 — с нуля: пустая БД → upgrade head — проверяет целостность цепочки (история ревизий, зависимости down_revision, отсутствие «ветвления» после слияний веток — типовой баг после параллельных PR с миграциями: две ревизии с одним родителем — alembic требует merge-ревизию, CI ловит до прода). Прогон 2 — обратимость: downgrade новой ревизии и повторный upgrade — downgrade работает и идемпотентен по эффекту (для expand/contract-ревизий downgrade обязателен — практика: откат деплоя без отката схемы — контракт команды; «downgrade = pass» молча — обман, который вскроется в инциденте). Прогон 3 — с данными: схема N-1 + реалистичные данные (строки с NULL/дублями/краевыми значениями в затрагиваемых колонках) → upgrade → assert'ы на результат (колонка заполнена, ограничения выполнены, скорость приемлема — миграция на 10 строках мгновенна, на 10 млн — час: замер времени на объёме — часть теста, иначе «быстрая» миграция кладёт деплой-окно). Автоматизация: pytest-тест, поднимающий контейнер, гоняющий alembic-команды (alembic API программно), проверяющий состояние через inspector/запросы; каждый новый миграционный PR прогоняет свой дифф ревизий — стоимость минуты CI против аварийного отката.

Factory-подход: данные как код

Проблема фикстур-дампов: SQL-дамп схемы+данных (или JSON) — чёрный ящик («какие поля важны?» непонятно), хрупкость (смена схемы — правка дампа руками по всем тестам), нечитаемость ревью («в дампе 400 строк изменились — что по смыслу?»). Factory: class OrderFactory(SQLAlchemyModelFactory): class Meta: model = Order; customer = SubFactory(CustomerFactory); status = "new"; total = Faker("pydecimal", 2) — объект создаётся с валидными связями; тест переопределяет только значимое: OrderFactory(status="paid", total=0) — намерение видно в строке; изменение схемы — правка фабрики в одном месте (все тесты обновлены опосредованно). Слои: фабрики базовых сущностей (customer/order/product) + трейты/варианты (OrderFactory.build_batch(50, status="cancelled") — объёмные наборы для нагрузочных кейсов); связанные графы — SubFactory (заказ тянет клиента, клиент — компанию); Faker для правдоподобия (телефоны/email/адреса — без «test1/test2», на которых ломаются валидаторы). Граница: фабрики — для unit/интеграционных тестов; продакшен-сид (справочники, демо-данные стендов) — отдельный версионируемый код (management-команда с фабрика-подобными записями, идемпотентная — повторный запуск не дублирует — идемпотентность); дамп как fixture оправдан только для «точное состояние прода для воспроизведения бага» — разово, не как система.

Транзакционная изоляция и скорость

Схема тестовой БД: session-фикстура поднимает контейнер и применяет миграции (один раз на сессию — скорость), function-фикстура — begins transaction → yield → rollback (каждый тест в откатываемой транзакции: данные не копятся, порядок запуска не важен, параллелизм — по базам/схемам); нюанс — код, открывающий свои соединения мимо тестовой транзакции (фоновые потоки/Celery-eager с отдельным conn — видят незакоммиченное или не видят ничего — такие интеграции тестируются с commit'ом и явной очисткой или отдельной БД). Скорость контура: тесты миграций — не в каждом запуске всех тестов (отдельная марка @pytest.mark.migrations — прогон на PR с миграциями и nightly — полный); testcontainers переиспользуется между тестами (reuse/сессия — не контейнер на тест); фабрики с build (без БД) для юнитов и create (с БД) для интеграций — разделение дешёвое и дорогое. Метрика здоровья: полный контур < 5–10 минут (иначе тесты перестают запускать локально — CI-матрица держит время в рамках параллелизмом).

Практический сценарий: миграция с data-бэкфиллом

Задача: ревизия добавляет orders.channel (not null, default 'web') и заполняет исторические значения из JSON-поля attrs. Без тестов: на проде 3 млн заказов, среди attrs — 12k строк без ключа channel и 400 с channel=null — миграция с attrs['channel'] без coalesce падает на середине (not null violation), транзакция откатывается, деплой сорван в окне. С тестами: миграционный тест готовит данные-ловушки (attrs без ключа / null / нормальный / невалидный JSON — четыре строки-архетипа), upgrade — и assert: not-null соблюдён, fallback 'unknown' проставлен, остальные значения перенесены, время на 100k синтетических — экстраполяция приемлемая; downgrade — колонка удалена, данные attrs не тронуты. CI на PR: прогон 1–3 зелёные — миграция ушла в прод предсказуемо; на проде 3 млн — 4 минуты, в пределах окна. Побочный эффект культуры: фабрики заказов получили трейты attrs_broken/attrs_null (ловушки стали частью тестового набора — следующие миграции attrs проверяются на них автоматически); data-миграции в команде теперь пишутся с тестами по шаблону — инцидент не повторился на трёх следующих ревизиях с бэкфиллом.

FAQ

Тестировать ли каждую миграцию или только «сложные»? Цепочку (upgrade head/downgrade новой) — каждую автоматически (стоимость нулевая после настройки); data-бэкфиллы и DDL на больших таблицах — с данными и замером времени обязательно; «простая» add column nullable — covered прогоном цепочки; классификация «сложная/простая» подводит реже, когда тестов много, — автоматика решает.

SQL-фикстуры совсем не нужны? Нужны в нишах: воспроизведение конкретного прод-состояния (баг-репорт с дампом), тестирование сырых SQL-скриптов/представлений (данные как вход скрипта), эталонные наборы для отчётов (golden data — эвалы по духу); система по умолчанию — фабрики, SQL — инструмент под задачу.

Как тестировать миграции, которые нельзя откатить (drop column с данными)? Contract-фаза по определению необратима — тестируется обратимость expand-фазы и корректность contract на копии данных (тест: данные до/после — удалено только задуманное); в проде contract-ревизия идёт после полного раската нового кода — деплой-дисциплина делает необратимость безопасной по времени.

Нужны ли отдельные фикстуры для нагрузочных тестов? Да: объёмные наборы генерируются фабрика-батчами с реалистичным распределением (не 100k одинаковых строк — планировщик БД и кеш ведут себя на правдоподобных данных иначе — планы запросов); хранение — генерация на лету или отдельная «объёмная» база вне основного контура (локально — docker-volume с предзаполненной БД — скорость против свежести, обновляется по схеме).

Источники: документация Alembic, pytest fixtures HOWTO, factory_boy — ссылки в шапке.