John Mee
John Mee

Reputation: 52253

How to unittest a django database migration?

We've changed our database, using django migrations (django v1.7+). The data that exists in the database is no longer valid.

Basically I want to test a migration by, inside a unittest, constructing the pre-migration database, adding some data, applying the migration, then confirming everything went smoothly.

How does one:

  1. hold back the new migration when loading the unittest

    I found some stuff about overriding settings.MIGRATION_MODULES but couldn't work out how to use it. When I inspect executor.loader.applied_migrations it still lists everything. The only way I could prevent the new migration was to actually remove the file; not a solution I can use.

  2. create a record in the unittest database (using the old model)

    If we can prevent the migration then this should be pretty straightforward. myModel.object.create(...)

  3. apply the migration

    I think I can probably work this out now that I've found the test_executor: set a plan pointing to the migration file and execute it? Um, right? Got any code for that :-D

  4. confirm the old data in the database now matches the new model

    Again, I expect this should be pretty easy: just fetch the instance created before the migration and confirm it has changed in all the right ways.

So the challenge is really just working out how to prevent the unittest from applying the latest migration script and then applying it when we're ready?


Perhaps I have the wrong approach? Should I create fixtures, and just confirm that they're all good at the end? Do fixtures get loaded before the migrations are applied, or after they're all done?


By using the MigrationExecutor and picking out specific migrations with .migrate I've been able to, maybe?, roll it back to a specific state, then roll forward one-by-one. But that is popping up doubts; currently chasing down sqlite fudging around due to the lack of an actual ALTER TABLE instruction. Jury still out.

Upvotes: 6

Views: 1454

Answers (2)

John Mee
John Mee

Reputation: 52253

I wasn't able to prevent the unittest from starting with the current database schema, but I did find it is quite easy to revert to earlier points in the migration history:

Where "0014_nulls_permitted" is a file in the migrations directory...

from django.db.migrations.executor import MigrationExecutor
executor.migrate([("workflow_engine", "0014_nulls_permitted")])
executor.loader.build_graph()

NB: running the executor.loader.build_graph between invocations of executor.migrate seems to be a very important part of completing the migration and making things behave as one might expect

The migrations which are currently applicable to the database can be checked with something like:

print [x[1] for x in sorted(executor.loader.applied_migrations)]

[u'0001_initial', u'0002_fix_foreignkeys', ... u'0014_nulls_permitted']

I created a model instance via the ORM then ensured the database was in the old state by running some SQL directly:

job = Job.objects.create(....)
from django.db import connection
cursor = connection.cursor()
cursor.execute('UPDATE workflow_engine_job SET next_job_state=NULL')

Great. Now I know I have a database in the old state, and can test the forwards migration. So where 0016_nulls_banished is a migration file:

executor.migrate([("workflow_engine", "0016_nulls_banished")])
executor.loader.build_graph()

Migration 0015 goes through the database converting all the NULL fields to a default value. Migration 0016 alters the schema. You can scatter some print statements around to confirm things are happening as you think they should be.

And now the test can confirm that the migration has worked. In this case by ensuring there are no nulls left in the database.

jobs = Job.objects.all()
self.assertTrue(all([j.next_job_state is not None for j in jobs]))

Upvotes: 5

zsepi
zsepi

Reputation: 1662

We have used the following code in settings_test.py to ignore the migration for the tests:

MIGRATION_MODULES = dict(
    (app.split('.')[-1], '.'.join([app, 'nonexistent_django_migrations_module']))
    for app in INSTALLED_APPS
)

The idea here being that none of the apps have a nonexistent_django_migrations_module folder, and thus django will simply find no migrations.

Upvotes: -1

Related Questions