关于python:如何针对Django数据迁移运行测试?

How do I run tests against a Django data migration?

using the following example from the documentation。P></

1
2
3
4
5
6
7
8
9
10
11
12
13
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),
    ]

如何创建和运行所有的测试,这对confirming is the),migrated日期正确吗?P></


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

乍一看,这不应该比普通的Django单元测试困难得多:迁移是Python模块,migrations/文件夹是一个包,因此可以从中导入东西。然而,这项工作花了一些时间。

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

1
from yourappname.migrations.0002_my_data_migration import combine_names

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

至少有两种方法可以实现这一点:

  • 重命名迁移文件,使其不以数字开头。根据文档,这应该是完全正确的:"Django只关心每个迁移有不同的名称。"然后您可以像上面那样使用import

  • 如果您想坚持使用默认编号的迁移文件名,可以使用python的import_module(参见docs和这个so问题)。

  • 第二个困难来自这样一个事实:您的数据迁移函数被设计为被传递到RunPython文档中,因此它们默认需要两个输入参数:appsschema_editor。要查看这些信息来自何处,可以检查其来源。

    现在,我不确定这是否适用于每一个案例(请任何人,如果你能澄清的话,请发表评论),但是对于我们的案例,从django.apps导入apps并从活动数据库connection获取schema_editor就足够了。

    下面是一个简化的示例,展示了如何为OP示例实现这一点,假设迁移文件名为0002_my_data_migration.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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)

    我在谷歌上做了一些工作来解决同一个问题,找到了一篇为我钉钉子的文章,看起来不像现有答案那么简单。所以,把这个放在这里,以防它能帮助其他人。

    Django's TestCase的以下子类建议:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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")


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

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

    对于更"完整"的解决方案,这篇较旧的博客文章有一些好的信息和更多关于灵感的最新评论:

    https://micknelson.wordpress.com/2013/03/01/testing-django-migrations/评论