![Oracle Drags Its Feet in the JavaScript Trademark Dispute](https://cdn.sanity.io/images/cgdhsj6q/production/919c3b22c24f93884c548d60cbb338e819ff2435-1024x1024.webp?w=400&fit=max&auto=format)
Security News
Oracle Drags Its Feet in the JavaScript Trademark Dispute
Oracle seeks to dismiss fraud claims in the JavaScript trademark dispute, delaying the case and avoiding questions about its right to the name.
django-test-migrations
Advanced tools
django
schema and data migrationsmypy
, PEP561 compatibleRead the announcing post. See real-world usage example.
pip install django-test-migrations
We support several django
versions:
3.2
4.1
4.2
5.0
Other versions most likely will work too, but they are not officially supported.
Testing migrations is not a frequent thing in django
land.
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.
To test all migrations we have a Migrator
class.
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 the before state
by 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 testing
So, here's an example:
from django_test_migrations.migrator import Migrator
migrator = 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')
assert len(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:
assert SomeItem.objects.filter(is_clean=True).count() == 0
assert len(SomeItem._meta.get_fields()) == 3 # id + string_field + is_clean
# Cleanup:
migrator.reset()
That was an example of a forward migration.
The 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()
Sometimes we also want to be sure that our migrations are in the correct order
and that all our dependencies = [...]
are correct.
To achieve that we have plan.py
module.
That's how it can be used:
from django_test_migrations.plan import all_migrations, nodes_to_tuples
main_migrations = all_migrations('default', ['main_app', 'other_app'])
assert nodes_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_boy
integrationIf you use factories to create models, you can replace their respective
.build()
or .create()
calls with methods of factory
and pass the
model name and factory class as arguments:
import factory
old_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)
# ...
We support several test frameworks as first-class citizens. That's a testing tool after all!
Note that the Django post_migrate
signal's receiver list is cleared at
the start of tests and restored afterwards. If you need to test your
own post_migrate
signals then attach/remove them during a test.
We ship django-test-migrations
with a pytest
plugin
that provides two convenient fixtures:
migrator_factory
that gives you an opportunity
to create Migrator
classes for any databasemigrator
instance for the 'default'
databaseThat's how it can be used:
import pytest
@pytest.mark.django_db
def test_pytest_plugin_initial(migrator):
"""Ensures that the initial migration works."""
old_state = migrator.apply_initial_migration(('main_app', None))
with pytest.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')
assert SomeItem.objects.filter(string_field='').count() == 0
We also ship an integration with the built-in unittest
framework.
Here's how it can be used:
from django_test_migrations.contrib.unittest_case import MigratorTestCase
class TestDirectMigration(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')
def prepare(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')
def test_migration_main0003(self):
"""Run the test itself."""
SomeItem = self.new_state.apps.get_model('main_app', 'SomeItem')
assert SomeItem.objects.count() == 2
assert SomeItem.objects.filter(is_clean=True).count() == 1
In 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.
django_test_migrations
adds migration_test
marker to each test using
migrator_factory
or migrator
fixture.
To run only migrations test, use -m
option:
pytest -m migration_test # Runs only migration tests
pytest -m "not migration_test" # Runs all except migration tests
django_test_migrations
adds migration_test
tag
to every MigratorTestCase
subclass.
To run only migrations tests, use --tag
option:
python mange.py test --tag=migration_test # Runs only migration tests
python mange.py test --exclude-tag=migration_test # Runs all except migration tests
django_test_migrations
comes with 2 groups of Django's checks for:
django
generates migration names for you when you run makemigrations
.
These names are bad (read more about why it is bad)!
Just look at this: 0004_auto_20191119_2125.py
What does this migration do? What changes does it have?
One can also pass --name
attribute when creating migrations, but it is easy to forget.
We offer an automated solution: django
check
that produces an error for each badly named migration.
Add our check into your INSTALLED_APPS
:
INSTALLED_APPS = [
# ...
# Our custom check:
'django_test_migrations.contrib.django_checks.AutoNames',
]
Then in your CI run:
python manage.py check --deploy
This 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.py
DTM_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.py
DTM_IGNORED_MIGRATIONS = {
('dependency_app', '*'),
('another_dependency_app', '*'),
}
Add our check to INSTALLED_APPS
:
INSTALLED_APPS = [
# ...
# Our custom check:
'django_test_migrations.contrib.django_checks.DatabaseConfiguration',
]
Then just run check
management command in your CI like listed in section
above.
You might also like:
django-test-migrations
and django-migration-linter
on board.This project is based on work of other awesome people:
MIT.
FAQs
Test django schema and data migrations, including ordering
We found that django-test-migrations demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Oracle seeks to dismiss fraud claims in the JavaScript trademark dispute, delaying the case and avoiding questions about its right to the name.
Security News
The Linux Foundation is warning open source developers that compliance with global sanctions is mandatory, highlighting legal risks and restrictions on contributions.
Security News
Maven Central now validates Sigstore signatures, making it easier for developers to verify the provenance of Java packages.