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

时间:2017-05-16 14:00:18

标签: python django unit-testing database-migration

使用documentation中的以下示例:

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Product;
use App\ProductTrans;
use App\Service;
use Spatie\LaravelAnalytics\LaravelAnalytics;

class HomeController extends Controller {

    //
    public function getIndex() {
        $services = Service::get();
        $products = Product::get();
        $analyticsData =new LaravelAnalytics();
        $analyticsData->getVisitorsAndPageViews();
        dd($analyticsData);

        return view('admin.pages.home',compact('services','products'));
    }

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

4 个答案:

答案 0 :(得分:2)

您可以使用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

答案 1 :(得分:1)

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

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

first 困难是由于默认迁移文件名以数字开头而引起的。例如,假设来自OP(即Django)数据迁移示例的代码位于0002_my_data_migration.py中,那么使用

很诱人
from yourappname.migrations.0002_my_data_migration import combine_names

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

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

  1. 重命名迁移文件,因此它不能以数字开头。根据{{​​3}},这应该是完全可以的:“ Django只是在乎每个迁移都使用不同的名称。”然后,您可以如上所述使用import

  2. 如果您要保留默认编号的迁移文件名,则可以使用Python的import_module(请参阅docsdocs SO问题)。

    < / li>

第二个 困难来自以下事实:您的数据迁移函数旨在传递给RunPythonthis),因此默认情况下它们需要两个输入参数:appsschema_editor。要查看它们的来源,您可以检查docs

现在,我不确定这是否适用于所有情况(请澄清,如果可以澄清,请发表评论),但是对于我们的情况,从source导入apps并获取活动数据库schema_editordjango.apps)中的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)

答案 2 :(得分:0)

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

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

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

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

答案 3 :(得分:0)

我正在做一个Google来解决相同的问题,发现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")