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

Osk*_*son 8 python django unit-testing database-migration

使用文档中的以下示例:

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),
    ]
Run Code Online (Sandbox Code Playgroud)

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

dev*_*inm 11

我正在做一些谷歌来解决同样的问题,并找到了一篇文章,它为我钉在钉子上,似乎没有现有答案那么笨拙。所以,把它放在这里以防它帮助其他人。

提出了 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
Run Code Online (Sandbox Code Playgroud)

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

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")
Run Code Online (Sandbox Code Playgroud)


djv*_*jvg 7

编辑:

这些其他答案更有意义:

原来的:

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

乍一看,这应该不会比普通的 Django 单元测试困难多少:迁移是 Python 模块,文件migrations/夹是一个包,因此可以从中导入内容。然而,这个工作需要一些时间才能实现。

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

from yourappname.migrations.0002_my_data_migration import combine_names
Run Code Online (Sandbox Code Playgroud)

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

至少有两种方法可以实现这项工作:

  1. 重命名迁移文件,使其不以数字开头。根据文档,这应该完全没问题 “Django 只是关心每个迁移都有不同的名称。” 然后就可以import像上面那样使用了。

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

第二困难源于这样一个事实:您的数据迁移函数被设计为传递到RunPython( docs ),因此它们默认需要两个输入参数:appsschema_editor。要了解它们的来源,您可以检查来源

现在,我不确定这是否适用于每种情况(如果您能澄清,请任何人发表评论),但对于我们的情况,appsdjango.appsschema_editor导入并从活动数据库connection( django.db )获取就足够了。联系)。

以下是一个精简示例,展示了如何在 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)
        
Run Code Online (Sandbox Code Playgroud)


sob*_*evn 6

You can use django-test-migrations package. 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
Run Code Online (Sandbox Code Playgroud)

We also have native integrations for both 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')
    ...
Run Code Online (Sandbox Code Playgroud)

And 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
Run Code Online (Sandbox Code Playgroud)

  • @RafaelAlmeida 这是一个很好的提醒,任何仍在开发中的严肃应用程序现在都应该迁移到 Python 3。 (7认同)
  • @RafaelAlmedia,该包支持 Django 2.2、3.1、3.2 和 4.0 - 这些 Django 版本都不支持 Python 2,因此 Python 2 支持是多余的。还值得注意的是,任何支持 Python 2 的 Django 版本都已结束生命。 (2认同)

whp*_*whp 0

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

import sys
if 'test in sys.argv:
    # do steps to update your operations
Run Code Online (Sandbox Code Playgroud)

对于更“完整”的解决方案,这篇较旧的博客文章提供了一些很好的信息和更多最新的灵感评论:

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