utils.testhelpers — Testing utilities

Testing Django migrations

Warning

You can not test migrations if you have a MIGRATION_MODULES setting that disabled migrations. So make sure you remove that setting if you have it in your test settings.

Guide

Lets say you have the following model:

class Node(models.Model):
    name = models.CharField(max_length=255)

You have an initial migration, and you have created a migration named 0002_suffix_name_with_stuff which looks like this:

suffix = ' STUFF'

def add_stuff_to_all_node_names(apps, schema_editor):
    Node = apps.get_model('myapp', 'Node')
    for node in Node.objects.all():
        node.name = '{}{}'.format(node.name, suffix)
        node.save()

def reverse_add_stuff_to_all_node_names(apps, schema_editor):
    Node = apps.get_model('myapp', 'Node')
    for node in Node.objects.all():
        if node.name.endswith(suffix):
            node.name = node.name[:-len(suffix)]
            node.save()

class Migration(migrations.Migration):
    dependencies = [
        ('myapp', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(add_stuff_to_all_node_names, reverse_code=reverse_add_stuff_to_all_node_names),
    ]

Note

You can not test migrations that can not be reversed, so you MUST write reversible migrations if you want to be able to test them. Think of this as a good thing - it forces you to write reversible migrations.

To test this, you can write a test case like this:

from ievv_opensource.utils.testhelpers import testmigrations

class TestSomeMigrations(testmigrations.MigrationTestCase):
    app_label = 'myapp'
    migrate_from = '0001_initial'
    migrate_to = '0002_suffix_name_with_stuff'

    def test_migrate_works(self):

        # Add some data to the model using the ``apps_before`` model state
        Node = self.apps_before.get_model('myapp', 'Node')
        node1_id = Node.objects.create(
            name='Node1'
        ).id
        node2_id = Node.objects.create(
            name='Node2'
        ).id

        # Migrate (run the 0002_suffix_name_with_stuff migration)
        self.migrate()

        # Test using the ``apps_after`` model state.
        Node = self.apps_after.get_model('myapp', 'Node')
        self.assertEqual(Node.objects.get(id=node1_id).name, 'Node1 STUFF')
        self.assertEqual(Node.objects.get(id=node2_id).name, 'Node2 STUFF')

    def test_reverse_migrate_works(self):

        # First, we migrate to get to a state where we can reverse the migration
        self.migrate()

        # Add some data to the model using the ``apps_after`` model state
        Node = self.apps_after.get_model('myapp', 'Node')
        node1_id = Node.objects.create(
            name='Node1 STUFF'
        ).id
        node2_id = Node.objects.create(
            name='Node2 STUFF'
        ).id

        # Reverse the migration
        self.reverse_migrate()

        # Test using the ``apps_before`` model state.
        Node = self.apps_before.get_model('myapp', 'Node')
        self.assertEqual(Node.objects.get(id=node1_id).name, 'Node1')
        self.assertEqual(Node.objects.get(id=node2_id).name, 'Node2')

The MigrationTestCase class

class ievv_opensource.utils.testhelpers.testmigrations.MigrationTestCase(methodName='runTest')[source]

Bases: django.test.testcases.TransactionTestCase

Test case for a Django database migration.

Example:

class TestSomeMigrations(MigrationTestCase):
    migrate_from = '0002_previous_migration'
    migrate_to = '0003_migration_being_tested'

    def test_is_selected_is_flipped(self):
        MyModel = self.apps_before.get_model('myapp', 'MyModel')
        MyModel.objects.create(
            name='Test1',
            is_selected=True
        )
        MyModel.objects.create(
            name='Test2',
            is_selected=False
        )
        MyModel.objects.create(
            name='Test3',
            is_selected=True
        )

        self.migrate()

        MyModel = self.apps_after.get_model('myapp', 'MyModel')
        self.assertEqual(MyModel.objects.filter(is_selected=True).count, 1)
        self.assertEqual(MyModel.objects.filter(is_selected=False).count, 2)

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

app_label = None

The django app_label for the app you are migrating. This is the same app_label as you use with python manage.py makemigrations <app_label> to create the migration.

migrate_dependencies = None

Dependencies. A list of (app_label, migration_name) tuples.

migrate_from_dependencies = None

Same as migrate_dependencies, but ONLY for migrate_from.

migrate_to_dependencies = None

Same as migrate_dependencies, but ONLY for migrate_from.

migrate_from = None

The name of the migration to migrate from. Can be the full name, or just the number (I.E.: 0002 or 0002_something.

migrate_to = None

The name of the migration to migrate to. Can be the full name, or just the number (I.E.: 0003 or 0003_something.

classmethod setUpClass()[source]

Hook method for setting up class fixture before running tests in the class.

setUp()[source]

Perform required setup.

If you override setUp(), you must call super().setUp()!

apps_before

Get an apps object just like the first argument to a Django data migration at the state before migration has been run.

Only available before migrate() has been called, or after reverse_migrate() has been called.

apps_after

Get an apps object just like the first argument to a Django data migration at the state after migration has been run, and not available after reverse_migrate() has been called (unless migrate() is called again).

Only available after migrate() has been called.

migrate()[source]

Migrate the database from migrate_from to migrate_to.

reverse_migrate()[source]

Migrate the database from migrate_to to migrate_from.

You must call migrate() before calling this.

get_migrate_command_kwargs()[source]

Get kwargs for the migrate management command.

The defaults are sane, by you may want to override this and change the verbosity argument for debugging purposes.