Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 568
John Mee Avatar asked May 12 '16 04:05

John Mee


1 Answers

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]))
like image 175
John Mee Avatar answered Oct 04 '22 07:10

John Mee