← Назад к вопросам

Как протестировать миграции в Django?

2.2 Middle🔥 151 комментариев
#Django#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Тестирование миграций в 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 для тестирования миграций

  1. Тестируй обе стороны: Forward и backward migrations
  2. Используй отдельную тестовую БД: Не трогай production
  3. Тестируй с реальными данными: Используй factory для создания тестовых данных
  4. Проверяй производительность: Большие таблицы могут зависнуть при миграции
  5. Документируй сложные миграции: Комментариями объясняй логику
  6. Используй RunSQL осторожно: Если используешь сырой SQL, тестируй на обеих БД (PostgreSQL, MySQL)
  7. 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 может затронуть миллионы пользователей!

Как протестировать миграции в Django? | PrepBro