简体   繁体   English

如何针对 Django 数据迁移运行测试?

[英]How do I run tests against a Django data migration?

Using the following example from the documentation :使用文档中的以下示例:

def combine_names(apps, schema_editor):
    Person = apps.get_model("yourappname", "Person")
    for person in Person.objects.all():
        person.name = "%s %s" % (person.first_name, person.last_name)
        person.save()

class Migration(migrations.Migration):    
    dependencies = [
        ('yourappname', '0001_initial'),
    ]    
    operations = [
        migrations.RunPython(combine_names),
    ]

How would I create and run a test against this migration, confirming that the data is migrated correctly?我将如何针对此迁移创建和运行测试,以确认数据已正确迁移?

I was doing some google to address the same question and found an article that nailed the hammer on the nail for me and seemed less hacky than existing answers.我正在做一些谷歌来解决同样的问题,并找到了一篇文章,它为我钉在钉子上,似乎没有现有答案那么笨拙。 So, putting this here in case it helps anyone else coming though.所以,把它放在这里以防它帮助其他人。

The proposed the following subclass of Django's TestCase :提出了 Django 的TestCase的以下子类:

from django.apps import apps
from django.test import TestCase
from django.db.migrations.executor import MigrationExecutor
from django.db import connection


class TestMigrations(TestCase):

    @property
    def app(self):
        return apps.get_containing_app_config(type(self).__module__).name

    migrate_from = None
    migrate_to = None

    def setUp(self):
        assert self.migrate_from and self.migrate_to, \
            "TestCase '{}' must define migrate_from and migrate_to     properties".format(type(self).__name__)
        self.migrate_from = [(self.app, self.migrate_from)]
        self.migrate_to = [(self.app, self.migrate_to)]
        executor = MigrationExecutor(connection)
        old_apps = executor.loader.project_state(self.migrate_from).apps

        # Reverse to the original migration
        executor.migrate(self.migrate_from)

        self.setUpBeforeMigration(old_apps)

        # Run the migration to test
        executor = MigrationExecutor(connection)
        executor.loader.build_graph()  # reload.
        executor.migrate(self.migrate_to)

        self.apps = executor.loader.project_state(self.migrate_to).apps

    def setUpBeforeMigration(self, apps):
        pass

And an example use case that they proposed was:他们提出的一个示例用例是:

class TagsTestCase(TestMigrations):

    migrate_from = '0009_previous_migration'
    migrate_to = '0010_migration_being_tested'

    def setUpBeforeMigration(self, apps):
        BlogPost = apps.get_model('blog', 'Post')
        self.post_id = BlogPost.objects.create(
            title = "A test post with tags",
            body = "",
            tags = "tag1 tag2",
        ).id

    def test_tags_migrated(self):
        BlogPost = self.apps.get_model('blog', 'Post')
        post = BlogPost.objects.get(id=self.post_id)

        self.assertEqual(post.tags.count(), 2)
        self.assertEqual(post.tags.all()[0].name, "tag1")
        self.assertEqual(post.tags.all()[1].name, "tag2")

You can usedjango-test-migrations package.您可以使用django-test-migrations包。 It is suited for testing: data migrations, schema migrations, and migrations' order .它适用于测试:数据迁移、模式迁移和迁移顺序

Here's how it works:这是它的工作原理:

from django_test_migrations.migrator import Migrator

# You can specify any database alias you need:
migrator = Migrator(database='default')

old_state = migrator.before(('main_app', '0002_someitem_is_clean'))
SomeItem = old_state.apps.get_model('main_app', 'SomeItem')

# One instance will be `clean`, the other won't be:
SomeItem.objects.create(string_field='a')
SomeItem.objects.create(string_field='a b')

assert SomeItem.objects.count() == 2
assert SomeItem.objects.filter(is_clean=True).count() == 2

new_state = migrator.after(('main_app', '0003_auto_20191119_2125'))
SomeItem = new_state.apps.get_model('main_app', 'SomeItem')

assert SomeItem.objects.count() == 2
# One instance is clean, the other is not:
assert SomeItem.objects.filter(is_clean=True).count() == 1
assert SomeItem.objects.filter(is_clean=False).count() == 1

We also have native integrations for both pytest :我们还为pytest提供了本地集成

@pytest.mark.django_db
def test_main_migration0002(migrator):
    """Ensures that the second migration works."""
    old_state = migrator.before(('main_app', '0002_someitem_is_clean'))
    SomeItem = old_state.apps.get_model('main_app', 'SomeItem')
    ...

And unittest :unittest

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_auto_20191119_2125')

    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

EDIT:编辑:

These other answers make more sense:这些其他答案更有意义:

ORIGINAL:原来的:

Running your data-migration functions (such as combine_names from the OP's example) through some basic unit-tests, before actually applying them, makes sense to me too.在实际应用它们之前,通过一些基本的单元测试运行您的数据迁移函数(例如 OP 示例中的combine_names ),对我来说也很有意义。

At first glance this should not be much more difficult than your normal Django unit-tests: migrations are Python modules and the migrations/ folder is a package, so it is possible to import things from them.乍一看,这应该不会比普通的 Django 单元测试困难得多:迁移是 Python 模块,而migrations/文件夹是一个包,因此可以从中导入内容。 However, it took some time to get this working.但是,这需要一些时间才能发挥作用。

The first difficulty arises due to the fact that the default migration file names start with a number.一个困难是由于默认迁移文件名以数字开头。 For example, suppose the code from the OP's (ie Django's) data-migration example sits in 0002_my_data_migration.py , then it is tempting to use例如,假设来自 OP(即 Django 的)数据迁移示例的代码位于0002_my_data_migration.py ,那么很容易使用

from yourappname.migrations.0002_my_data_migration import combine_names

but that would raise a SyntaxError because the module name starts with a number ( 0 ).但这会引发SyntaxError因为模块名称以数字 ( 0 ) 开头。

There are at least two ways to make this work:至少有两种方法可以使这项工作:

  1. Rename the migration file so it does not start with a number.重命名迁移文件,使其不以数字开头。 This should be perfectly fine according to the docs : "Django just cares that each migration has a different name."根据文档,这应该完全没问题:“Django 只关心每个迁移都有不同的名称。” Then you can just use import as above.然后你可以像上面一样使用import

  2. If you want to stick to the default numbered migration file names, you can use Python's import_module (see docs and this SO question).如果您想坚持使用默认编号的迁移文件名,您可以使用 Python 的import_module (请参阅文档这个SO 问题)。

The second difficulty arises from the fact that your data-migration functions are designed to be passed into RunPython ( docs ), so they expect two input arguments by default: apps and schema_editor .第二个困难来自这样一个事实,即您的数据迁移函数被设计为传递到RunPython ( docs ),因此它们默认需要两个输入参数: appsschema_editor To see where these come from, you can inspect the source .要查看这些来自哪里,您可以检查

Now, I'm not sure this works for every case (please, anyone, comment if you can clarify), but for our case, it was sufficient to import apps from django.apps and get the schema_editor from the active database connection ( django.db.connection ).现在,我不知道这个工程的每一种情况下(请,任何人,评论,如果你能澄清),但对于我们而言,这是足够的导入appsdjango.apps并获得schema_editor从活动数据库connectionDjango的.db.connection )。

The following is a stripped-down example showing how you can implement this for the OP example, assuming the migration file is called 0002_my_data_migration.py :以下是一个精简的示例,展示了如何为 OP 示例实现这一点,假设迁移文件名为0002_my_data_migration.py

from importlib import import_module
from django.test import TestCase
from django.apps import apps
from django.db import connection
from yourappname.models import Person
# Our filename starts with a number, so we use import_module
data_migration = import_module('yourappname.migrations.0002_my_data_migration')


class DataMigrationTests(TestCase):
    def __init__(self, *args, **kwargs):
        super(DataMigrationTests, self).__init__(*args, **kwargs)
        # Some test values
        self.first_name = 'John'
        self.last_name = 'Doe'
        
    def test_combine_names(self):
        # Create a dummy Person
        Person.objects.create(first_name=self.first_name,
                              last_name=self.last_name, 
                              name=None)
        # Run the data migration function
        data_migration.combine_names(apps, connection.schema_editor())
        # Test the result
        person = Person.objects.get(id=1)
        self.assertEqual('{} {}'.format(self.first_name, self.last_name), person.name)
        

You could add a crude if statement to a prior migration that tests if the test suite is running, and adds initial data if it is -- that way you can just write a test to check if the objects are in the final state you want them in. Just make sure your conditional is compatible with production, here's an example that would work with python manage.py test :您可以在先前的迁移中添加一个粗略的 if 语句来测试测试套件是否正在运行,如果是,则添加初始数据——这样您就可以编写一个测试来检查对象是否处于您想要的最终状态in. 只需确保您的条件与生产兼容,这是一个适用于python manage.py test的示例:

import sys
if 'test in sys.argv:
    # do steps to update your operations

For a more "complete" solution, this older blog post has some good info and more up-to-date comments for inspiration:对于更“完整”的解决方案,这篇较旧的博客文章有一些很好的信息和更多最新的灵感评论:

https://micknelson.wordpress.com/2013/03/01/testing-django-migrations/#comments https://micknelson.wordpress.com/2013/03/01/testing-django-migrations/#comments

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM