【问题标题】:How do I run tests against a Django data migration?如何针对 Django 数据迁移运行测试?
【发布时间】:2017-10-15 15:54:30
【问题描述】:

使用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),
    ]

如何针对此迁移创建和运行测试,以确认数据已正确迁移?

【问题讨论】:

标签: python django unit-testing database-migration


【解决方案1】:

您可以在之前的迁移中添加一个粗略的 if 语句来测试测试套件是否正在运行,如果是,则添加初始数据 -- 这样您就可以编写一个测试来检查对象是否处于最终状态你想要它们。只要确保你的条件与生产兼容,这里有一个可以与python manage.py test一起使用的例子:

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

对于更“完整”的解决方案,这篇较早的博客文章有一些很好的信息和更多最新的 cmets 以获得灵感:

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

【讨论】:

    【解决方案2】:

    编辑:

    这些其他答案更有意义:

    原文:

    在实际应用它们之前,通过一些基本的单元测试运行您的数据迁移功能(例如 OP 示例中的 combine_names),这对我来说也很有意义。

    乍一看,这应该不会比普通的 Django 单元测试困难多少:迁移是 Python 模块,migrations/ 文件夹是一个包,因此可以从中导入东西。但是,它需要一些时间才能使其正常工作。

    第一个困难是由于默认迁移文件名以数字开头的事实。例如,假设 OP(即 Django 的)数据迁移示例中的代码位于 0002_my_data_migration.py,那么很容易使用

    from yourappname.migrations.0002_my_data_migration import combine_names
    

    但这会引发SyntaxError,因为模块名称以数字开头(0)。

    至少有两种方法可以做到这一点:

    1. 重命名迁移文件,使其不以数字开头。根据docs,这应该很好:“Django 只关心每个迁移都有不同的名称。”然后你可以像上面一样使用import

    2. 如果您想坚持使用默认编号的迁移文件名,可以使用 Python 的import_module(请参阅docsthis SO 问题)。

    第二个的困难在于您的数据迁移函数被设计为传递给RunPython (docs),因此默认情况下它们需要两个输入参数:appsschema_editor。要查看这些来自哪里,您可以查看source

    现在,我不确定这是否适用于所有情况(请任何人评论,如果你能澄清的话),但对于我们的情况,从 django.apps 导入 apps 并获得 schema_editor 就足够了来自活动数据库connection (django.db.connection)。

    以下是一个精简示例,展示了如何为 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)
            
    

    【讨论】:

    • 如果你只是想测试你的数据迁移,那么这个 sn-p 效果很好!
    • 如果稍后将Person.first_name 重命名为Person.firstname,此测试将停止工作尽管迁移和应用程序的其余部分仍然正确。这种测试是一次性的,一旦提交迁移就应该删除(最好与测试一起)。
    • @LutzPrechelt:你是对的。这就是为什么我尝试将人们派往其他答案的原因。 ;-)
    【解决方案3】:

    我正在做一些谷歌来解决同样的问题,并发现an article 为我钉上了钉子,似乎没有现有答案那么老套。所以,把它放在这里以防它帮助其他人。

    提出以下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
    

    他们提出的一个示例用例是:

    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")
    

    【讨论】:

    • 那篇文章很完美。不需要额外的包 - 所有原生 Django。我们跟着,它工作得很好。谢谢@devinm
    • 实际上,如果您计划测试数据更改,则此代码很好。测试架构更改的第二次,它失败了; django 会抱怨读写模型,因为它们与数据库不同步。 @sobolevn 的答案中提到的库处理得很好,我认为这使它成为一个更好的解决方案。
    【解决方案4】:

    您可以使用django-test-migrations 包。它适用于测试:数据迁移、模式迁移和migrations' order

    它是这样工作的:

    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
    

    我们还为pytest 提供了native integrations

    @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')
        ...
    

    还有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
    

    【讨论】:

    • 该库仅适用于python3 此解决方案不支持python2
    • @RafaelAlmeida 这是一个很好的提醒,任何仍在开发中的严肃应用程序现在都应该迁移到 Python 3。
    • @RafaelAlmedia,该软件包支持 Django 2.2、3.1、3.2 和 4.0 - 这些 Django 版本均不支持 Python 2,因此对 Python 2 的支持将是多余的。还值得注意的是,任何支持 Python 2 的 Django 版本都将停止使用。
    猜你喜欢
    • 1970-01-01
    • 2011-06-15
    • 1970-01-01
    • 2021-09-11
    • 1970-01-01
    • 2012-02-19
    • 1970-01-01
    • 2022-07-22
    • 2017-03-13
    相关资源
    最近更新 更多