Package: django-test-migrations
django-test-migrationsFeaturesAllows to testdjangoschema and data migrationsAllows to test both forward and rollback migrationsAllows to test the migrations orderAllows to test migration namesAllows to test database configurationFully typed with annotations and checked withmypy,PEP561 compatibleEasy to start: has lots of docs, tests, and tutorialsRead theannouncing post.
See real-worldusage example.Installationpipinstalldjango-test-migrationsWe support severaldjangoversions:2.23.24.04.14.2Other versions most likely will work too,
but they are not officially supported.Testing Django migrationsTesting migrations is not a frequent thing indjangoland.
But, sometimes it is totally required. When?When we do complex schema or data changes
and what to be sure that existing data won't be corrupted.
We might also want to be sure that all migrations can be safely rolled back.
And as a final touch, we want to be sure that migrations
are in the correct order and have correct dependencies.Testing forward migrationsTo test all migrations we have aMigratorclass.It has three methods to work with:.apply_initial_migration()which takes app and migration names to generate
a state before the actual migration happens. It creates thebefore stateby applying all migrations up to and including the ones passed as an argument..apply_tested_migration()which takes app and migration names to perform the
actual migration.reset()to clean everything up after we are done with testingSo, here's an example:fromdjango_test_migrations.migratorimportMigratormigrator=Migrator(database='default')# Initial migration, currently our model has only a single string field:# Note:# We are testing migration `0002_someitem_is_clean`, so we are specifying# the name of the previous migration (`0001_initial`) in the# .apply_initial_migration() method in order to prepare a state of the database# before applying the migration we are going to test.#old_state=migrator.apply_initial_migration(('main_app','0001_initial'))SomeItem=old_state.apps.get_model('main_app','SomeItem')# Let's create a model with just a single field specified:SomeItem.objects.create(string_field='a')assertlen(SomeItem._meta.get_fields())==2# id + string_field# Now this migration will add `is_clean` field to the model:new_state=migrator.apply_tested_migration(('main_app','0002_someitem_is_clean'),)SomeItem=new_state.apps.get_model('main_app','SomeItem')# We can now test how our migration worked, new field is there:assertSomeItem.objects.filter(is_clean=True).count()==0assertlen(SomeItem._meta.get_fields())==3# id + string_field + is_clean# Cleanup:migrator.reset()That was an example of a forward migration.Backward migrationThe thing is that you can also test backward migrations.
Nothing really changes except migration names that you pass and your logic:migrator=Migrator()# Currently our model has two field, but we need a rollback:old_state=migrator.apply_initial_migration(('main_app','0002_someitem_is_clean'),)SomeItem=old_state.apps.get_model('main_app','SomeItem')# Create some data to illustrate your cases:# ...# Now this migration will drop `is_clean` field:new_state=migrator.apply_tested_migration(('main_app','0001_initial'))# Assert the results:# ...# Cleanup:migrator.reset()Testing migrations orderingSometimes we also want to be sure that our migrations are in the correct order
and that all ourdependencies = [...]are correct.To achieve that we haveplan.pymodule.That's how it can be used:fromdjango_test_migrations.planimportall_migrations,nodes_to_tuplesmain_migrations=all_migrations('default',['main_app','other_app'])assertnodes_to_tuples(main_migrations)==[('main_app','0001_initial'),('main_app','0002_someitem_is_clean'),('other_app','0001_initial'),('main_app','0003_update_is_clean'),('main_app','0004_auto_20191119_2125'),('other_app','0002_auto_20191120_2230'),]This way you can be sure that migrations
and apps that depend on each other will be executed in the correct order.factory_boyintegrationIf you use factories to create models, you can replace their respective.build()or.create()calls with methods offactoryand pass the
model name and factory class as arguments:importfactoryold_state=migrator.apply_initial_migration(('main_app','0002_someitem_is_clean'),)SomeItem=old_state.apps.get_model('main_app','SomeItem')# instead of# item = SomeItemFactory.create()# use this:factory.create(SomeItem,FACTORY_CLASS=SomeItemFactory)# ...Test framework integrations 🐍We support several test frameworks as first-class citizens.
That's a testing tool after all!Note that the Djangopost_migratesignal's receiver list is cleared at
the start of tests and restored afterwards. If you need to test your
ownpost_migratesignals then attach/remove them during a test.pytestWe shipdjango-test-migrationswith apytestplugin
that provides two convenient fixtures:migrator_factorythat gives you an opportunity
to createMigratorclasses for any databasemigratorinstance for the'default'databaseThat's how it can be used:importpytest@pytest.mark.django_db()deftest_pytest_plugin_initial(migrator):"""Ensures that the initial migration works."""old_state=migrator.apply_initial_migration(('main_app',None))withpytest.raises(LookupError):# Model does not yet exist:old_state.apps.get_model('main_app','SomeItem')new_state=migrator.apply_tested_migration(('main_app','0001_initial'))# After the initial migration is done, we can use the model state:SomeItem=new_state.apps.get_model('main_app','SomeItem')assertSomeItem.objects.filter(string_field='').count()==0unittestWe also ship an integration with the built-inunittestframework.Here's how it can be used:fromdjango_test_migrations.contrib.unittest_caseimportMigratorTestCaseclassTestDirectMigration(MigratorTestCase):"""This class is used to test direct migrations."""migrate_from=('main_app','0002_someitem_is_clean')migrate_to=('main_app','0003_update_is_clean')defprepare(self):"""Prepare some data before the migration."""SomeItem=self.old_state.apps.get_model('main_app','SomeItem')SomeItem.objects.create(string_field='a')SomeItem.objects.create(string_field='a b')deftest_migration_main0003(self):"""Run the test itself."""SomeItem=self.new_state.apps.get_model('main_app','SomeItem')assertSomeItem.objects.count()==2assertSomeItem.objects.filter(is_clean=True).count()==1Choosing only migrations testsIn CI systems it is important to get instant feedback. Running tests that
apply database migration can slow down tests execution, so it is often a good
idea to run standard, fast, regular unit tests without migrations in parallel
with slower migrations tests.pytestdjango_test_migrationsaddsmigration_testmarker to each test usingmigrator_factoryormigratorfixture.
To run only migrations test, use-moption:pytest-mmigration_test# Runs only migration testspytest-m"not migration_test"# Runs all except migration testsunittestdjango_test_migrationsaddsmigration_testtagto everyMigratorTestCasesubclass.
To run only migrations tests, use--tagoption:pythonmange.pytest--tag=migration_test# Runs only migration testspythonmange.pytest--exclude-tag=migration_test# Runs all except migration testsDjango Checksdjango_test_migrationscomes with 2 groups of Django's checks for:detecting migrations scripts automatically generated namesvalidating some subset of database settingsTesting migration namesdjangogenerates migration names for you when you runmakemigrations.
These names are bad (read moreabout why it is bad)!
Just look at this:0004_auto_20191119_2125.pyWhat does this migration do? What changes does it have?One can also pass--nameattribute when creating migrations, but it is easy to forget.We offer an automated solution:djangocheck
that produces an error for each badly named migration.Add our check into yourINSTALLED_APPS:INSTALLED_APPS=[# ...# Our custom check:'django_test_migrations.contrib.django_checks.AutoNames',]Then in your CI run:pythonmanage.pycheck--deployThis way you will be safe from wrong names in your migrations.Do you have a migrations that cannot be renamed? Add them to the ignore list:# settings.pyDTM_IGNORED_MIGRATIONS={('main_app','0004_auto_20191119_2125'),('dependency_app','0001_auto_20201110_2100'),}Then we won't complain about them.Or you can completely ignore entire app:# settings.pyDTM_IGNORED_MIGRATIONS={('dependency_app','*'),('another_dependency_app','*'),}Database configurationAdd our check toINSTALLED_APPS:INSTALLED_APPS=[# ...# Our custom check:'django_test_migrations.contrib.django_checks.DatabaseConfiguration',]Then just runcheckmanagement command in your CI like listed in section
above.Related projectsYou might also like:django-migration-linter- Detect backward incompatible migrations for your django project.wemake-django-template- Bleeding edge django template focused on code quality and security with bothdjango-test-migrationsanddjango-migration-linteron board.CreditsThis project is based on work of other awesome people:@asfaltboy@blueyed@fernandogrd@adamchainzLicenseMIT.