Skip to content

Instantly share code, notes, and snippets.

@asfaltboy
Last active March 30, 2022 23:52
Show Gist options
  • Save asfaltboy/b3e6f9b5d95af8ba2cc46f2ba6eae5e2 to your computer and use it in GitHub Desktop.
Save asfaltboy/b3e6f9b5d95af8ba2cc46f2ba6eae5e2 to your computer and use it in GitHub Desktop.
A pytest fixture to test Django data migrations
# based on https://gist.github.com/blueyed/4fb0a807104551f103e6
# and on https://gist.github.com/TauPan/aec52e398d7288cb5a62895916182a9f (gistspection!)
from django.core.management import call_command
from django.db import connection
from django.db.migrations.executor import MigrationExecutor
import pytest
@pytest.fixture()
def migration(request, transactional_db):
# see https://gist.github.com/asfaltboy/b3e6f9b5d95af8ba2cc46f2ba6eae5e2
"""
This fixture returns a helper object to test Django data migrations.
The fixture returns an object with two methods;
- `before` to initialize db to the state before the migration under test
- `after` to execute the migration and bring db to the state after the migration
The methods return `old_apps` and `new_apps` respectively; these can
be used to initiate the ORM models as in the migrations themselves.
For example:
def test_foo_set_to_bar(migration):
old_apps = migration.before([('my_app', '0001_inital')])
Foo = old_apps.get_model('my_app', 'foo')
Foo.objects.create(bar=False)
assert Foo.objects.count() == 1
assert Foo.objects.filter(bar=False).count() == Foo.objects.count()
# executing migration
new_apps = migration.apply([('my_app', '0002_set_foo_bar')])
Foo = new_apps.get_model('my_app', 'foo')
assert Foo.objects.filter(bar=False).count() == 0
assert Foo.objects.filter(bar=True).count() == Foo.objects.count()
Based on: https://gist.github.com/blueyed/4fb0a807104551f103e6
"""
class Migrator(object):
def before(self, targets):
""" Specify app and starting migration names as in:
before([('app', '0001_before')]) => app/migrations/0001_before.py
"""
self.executor = MigrationExecutor(connection)
# prepare state of db to before the migration ("migrate_from" state)
self._old_apps = self.executor.migrate(targets).apps
return self._old_apps
def apply(self, targets):
""" Migrate forwards to the "targets" migration """
self.executor.loader.build_graph() # reload.
self._new_apps = self.executor.migrate(targets).apps
return self._new_apps
# ensure to migrate forward migrated apps all the way after test
def migrate_to_end():
call_command('migrate', verbosity=0)
request.addfinalizer(migrate_to_end)
return Migrator()
@oliparcol
Copy link

On django-1.10.4, the example in the docstring is missing one level on the targets list:

        def test_foo_set_to_bar(migration):
            old_apps = migration.before([('my_app', '0001_inital')])
            ...
            new_apps = migration.apply([('my_app', '0002_set_foo_bar')])
            ...

@florimondmanca
Copy link

Thanks, very useful!

I encountered a missing import on call_command, had to add:

from django.core.management import call_command

to imports.

@asfaltboy
Copy link
Author

Thank you both, I updated the gist accordingly.

FWIW I noticed that as early as Django 1.8, migrate takes a list of target nodes, where each node is a tuple of (app_path, migration_name).

Ref: https://github.com/django/django/blob/1.8.19/django/db/migrations/graph.py#L91

@sobolevn
Copy link

I have made a pypi package out of this gist with some extra features.
Check it out: https://github.com/wemake-services/django-test-migrations

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment