Как протестировать миграции в Django?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Тестирование миграций в Django
Миграции — критическая часть любого проекта, так как они трансформируют schema БД и данные. Неправильная миграция может привести к потере данных, downtime или несогласованности. Поэтому их нужно тестировать тщательно.
Основные стратегии тестирования
1. Тестирование в изолированной БД
Используй отдельную БД для тестов, чтобы не потеть production:
# settings_test.py
from settings import *
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'test_db',
'USER': 'test_user',
'PASSWORD': 'test_password',
'HOST': 'localhost',
'PORT': '5432',
}
}
2. Unit-тесты для миграций
Джанго предоставляет класс TransactionTestCase для тестирования миграций:
from django.test import TransactionTestCase
from django.db.migrations.executor import MigrationExecutor
from django.db import connection
class MigrationTestCase(TransactionTestCase):
"""
Базовый класс для тестирования миграций.
Отключает транзакции для реального выполнения миграций.
"""
def setUp(self):
super().setUp()
self.executor = MigrationExecutor(connection)
def apply_migrations(self, app_label, migration_name):
"""Накатить миграцию до определённой версии"""
self.executor.migrate(
[(app_label, migration_name)],
plan=self.executor.migration_plan([(app_label, migration_name)])
)
def unapply_migrations(self, app_label, migration_name):
"""Откатить миграцию"""
self.executor.migrate(
[(app_label, None)],
plan=self.executor.migration_plan([(app_label, None)])
)
3. Тестирование вперёд (Forward Migration)
Тестируй, что миграция правильно применяется:
# tests/test_migrations.py
from django.test import TransactionTestCase
from django.apps import apps
from django.db import connection
from django.db.migrations.executor import MigrationExecutor
class CreateUserProfileMigrationTest(TransactionTestCase):
"""Тестирование миграции создания таблицы user_profile"""
migrate_from = '0001_initial'
migrate_to = '0002_create_user_profile'
app = 'accounts'
def setUp(self):
super().setUp()
self.executor = MigrationExecutor(connection)
# Откатываем до предыдущей миграции
self.executor.migrate([(self.app, self.migrate_from)])
def test_user_profile_table_created(self):
"""Проверяем, что таблица создалась"""
# Откатываемся перед миграцией
self.executor.migrate([(self.app, self.migrate_from)])
# user_profile ещё не существует
with self.assertRaises(ProgrammingError):
from django.db import connection
cursor = connection.cursor()
cursor.execute("SELECT * FROM accounts_userprofile LIMIT 1")
# Применяем нашу миграцию
self.executor.migrate([(self.app, self.migrate_to)])
# Теперь таблица существует
UserProfile = apps.get_model(self.app, 'UserProfile')
self.assertTrue(UserProfile)
# Проверяем поля
field_names = {f.name for f in UserProfile._meta.get_fields()}
self.assertIn('bio', field_names)
self.assertIn('avatar', field_names)
4. Тестирование назад (Backward Migration)
Тестируй, что откат работает правильно:
class RollbackMigrationTest(TransactionTestCase):
"""Тестирование отката миграции"""
migrate_from = '0002_create_user_profile'
migrate_to = '0001_initial'
app = 'accounts'
def test_rollback_removes_user_profile(self):
"""Откат должен удалить таблицу user_profile"""
executor = MigrationExecutor(connection)
# Применяем миграцию вперёд
executor.migrate([(self.app, self.migrate_from)])
UserProfile = apps.get_model(self.app, 'UserProfile')
self.assertTrue(UserProfile)
# Откатываемся
executor.migrate([(self.app, self.migrate_to)])
# Таблица больше не должна существовать
with self.assertRaises(LookupError):
apps.get_model(self.app, 'UserProfile')
5. Тестирование сохранения данных
При миграции с изменением схемы нужно проверить, что данные не потеряны:
class DataPreservationMigrationTest(TransactionTestCase):
"""Тестирование, что миграция сохраняет данные"""
migrate_from = '0003_user_email'
migrate_to = '0004_user_email_unique'
app = 'accounts'
def test_email_uniqueness_migration_preserves_data(self):
"""Добавление UNIQUE constraint не должно потерять данные"""
executor = MigrationExecutor(connection)
executor.migrate([(self.app, self.migrate_from)])
# Создаём тестовые данные
User = apps.get_model(self.app, 'User')
users = [
User.objects.create(name='Alice', email='alice@example.com'),
User.objects.create(name='Bob', email='bob@example.com'),
]
# Применяем миграцию
executor.migrate([(self.app, self.migrate_to)])
# Проверяем, что данные на месте
User = apps.get_model(self.app, 'User')
self.assertEqual(User.objects.count(), 2)
self.assertTrue(User.objects.filter(email='alice@example.com').exists())
self.assertTrue(User.objects.filter(email='bob@example.com').exists())
6. Тестирование с использованием pytest
Один из популярных подходов — pytest-django:
# conftest.py
import pytest
from django.db import connection
from django.db.migrations.executor import MigrationExecutor
@pytest.fixture
def migrator():
"""Fixture для тестирования миграций"""
return MigrationExecutor(connection)
# tests/test_migrations.py
import pytest
from django.apps import apps
from django.core.exceptions import FieldError
@pytest.mark.django_db(transaction=True)
def test_add_phone_field_migration(migrator):
"""Тест миграции добавления поля phone"""
# Откатываемся к 0001
migrator.migrate([('accounts', '0001_initial')])
User = apps.get_model('accounts', 'User')
# phone поля ещё нет
with pytest.raises(FieldError):
User.objects.values('phone')
# Применяем миграцию
migrator.migrate([('accounts', '0002_add_phone')])
# Теперь поле существует
User = apps.get_model('accounts', 'User')
assert 'phone' in [f.name for f in User._meta.get_fields()]
7. Интеграционное тестирование
Тестируй несколько миграций вместе:
class FullMigrationSequenceTest(TransactionTestCase):
"""Тестирование всего цикла миграций"""
app = 'accounts'
def test_complete_migration_sequence(self):
"""От 0001 до последней миграции все работает"""
executor = MigrationExecutor(connection)
# Получаем список всех миграций
targets = executor.loader.graph.leaf_nodes()
app_targets = [t for t in targets if t[0] == self.app]
# Откатываемся к началу
executor.migrate([(self.app, None)])
# Применяем все миграции по очереди
for target in app_targets:
executor.migrate([target])
# После каждой миграции проверяем консистентность
self._check_db_consistency()
def _check_db_consistency(self):
"""Проверка базовой консистентности БД"""
cursor = connection.cursor()
cursor.execute("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
""")
tables = cursor.fetchall()
self.assertGreater(len(tables), 0)
8. Тестирование с использованием factory_boy
Для создания тестовых данных используй factory:
import factory
from django.test import TransactionTestCase
from django.db.migrations.executor import MigrationExecutor
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'accounts.User'
email = factory.Sequence(lambda n: f'user{n}@example.com')
name = factory.Faker('name')
class UserMigrationWithDataTest(TransactionTestCase):
"""Тестирование миграции с реальными данными"""
def test_migration_with_many_users(self):
executor = MigrationExecutor(connection)
executor.migrate([('accounts', '0001_initial')])
# Создаём много тестовых пользователей
users = UserFactory.create_batch(100)
# Применяем миграцию (например, добавляем индекс)
executor.migrate([('accounts', '0002_add_email_index')])
# Проверяем, что все пользователи на месте
User = apps.get_model('accounts', 'User')
self.assertEqual(User.objects.count(), 100)
Best Practices для тестирования миграций
- Тестируй обе стороны: Forward и backward migrations
- Используй отдельную тестовую БД: Не трогай production
- Тестируй с реальными данными: Используй factory для создания тестовых данных
- Проверяй производительность: Большие таблицы могут зависнуть при миграции
- Документируй сложные миграции: Комментариями объясняй логику
- Используй RunSQL осторожно: Если используешь сырой SQL, тестируй на обеих БД (PostgreSQL, MySQL)
- CI/CD интеграция: Тестируй миграции в CI/CD pipeline перед деплоем
Пример CI/CD конфигурации
# .github/workflows/migrations.yml
name: Test Migrations
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: postgres
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.11'
- run: pip install -r requirements.txt
- run: python manage.py test tests.test_migrations --settings=settings_test
Заключение
Тестирование миграций — это обязательная практика, которая предотвращает проблемы в production. Используй TransactionTestCase, pytest-django, factory_boy и интегрируй тесты в CI/CD pipeline. Помни: миграция в production может затронуть миллионы пользователей!