How to unittest a django database migration?
Asked Answered
A

2

6

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.

Airflow answered 12/5, 2016 at 4:4 Comment(2)
Version control system might help in this situation. You will need to maintain two branches with old and new migrations. It looks similar to the case where you have separate branches for production and development environments with different settings, code, etc.Zany
@Zany checking out various commits doesn't typically fall within the scope of a unittest.Airflow
A
5

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]))
Airflow answered 13/5, 2016 at 1:56 Comment(1)
I assume that Jobs was previously set to something like apps.get_model('someapp.Job') for this to work. Correct? Otherwise, the Job model might not be compatible with the state of the schema.Lectureship
R
-1

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.

Roeder answered 12/5, 2016 at 6:1 Comment(4)
Thx. That works to ensure it can't find any migrations, but doesn't seem to help much; how can we test the migrations if it can't find them?Airflow
apparently I misunderstood your question - you actually want to test the migration logic itself?Roeder
Yes... a test which creates a pre-migration database, migrates it, then tests the result.Airflow
Gotcha. 2c: editing the title to reflect that would be helpful IMO :)Roeder

© 2022 - 2024 — McMap. All rights reserved.