Django:如何动态创建模型仅用于测试

Posted

技术标签:

【中文标题】Django:如何动态创建模型仅用于测试【英文标题】:Django: How to create a model dynamically just for testing 【发布时间】:2010-10-04 21:54:39 【问题描述】:

我有一个 Django 应用程序,它需要一个 settings 属性,格式为:

RELATED_MODELS = ('appname1.modelname1.attribute1',
                  'appname1.modelname2.attribute2', 
                  'appname2.modelname3.attribute3', ...)

然后根据定义的attributeN 挂钩他们的 post_save 信号以更新一些其他固定模型。

我想测试这种行为,即使这个应用程序是项目中唯一的一个,测试也应该可以工作(除了它自己的依赖项,不需要安装其他包装应用程序)。如何仅为测试数据库创建和附加/注册/激活模拟模型? (或者有可能吗?)

允许我使用测试夹具的解决方案会很棒。

【问题讨论】:

【参考方案1】:

您可以将测试放在应用程序的 tests/ 子目录中(而不是 tests.py 文件),并在仅测试模型中包含 tests/models.py

然后提供一个测试运行脚本 (example),其中在 INSTALLED_APPS 中包含您的 tests/“应用程序”。 (这在从真实项目运行应用程序测试时不起作用,INSTALLED_APPS 中没有测试应用程序,但我很少发现从项目运行可重用应用程序测试很有用,而 Django 1.6+ 没有默认情况下。)

(注意:下面描述的替代动态方法仅适用于 Django 1.1+,如果您的测试用例子类 TransactionTestCase - 这会显着减慢您的测试速度 - 并且在 Django 1.7 中不再适用+. 仅出于历史兴趣而留在这里;不要使用它。)

在测试开始时(即在 setUp 方法中,或在一组 doctest 开始时),您可以将 "myapp.tests" 动态添加到 INSTALLED_APPS 设置中,然后执行以下操作:

from django.core.management import call_command
from django.db.models import loading
loading.cache.loaded = False
call_command('syncdb', verbosity=0)

然后在测试结束时,您应该通过恢复旧版本的 INSTALLED_APPS 并再次清除应用缓存来进行清理。

This class 封装了该模式,因此它不会使您的测试代码非常混乱。

【讨论】:

这是一个干净而强大的snipplet(我猜它是你的)。起初创建一个完整的应用程序对于模拟模型来说似乎太多了。但现在我认为从单元测试的角度来看,它最能代表现实世界的使用情况。谢谢。 是的,我不知道什么是最好的,但这对我有用。当您意识到“创建一个完整的应用程序”的真正含义是“创建一个 models.py 文件”时,它似乎就不再那么重要了。 卡尔,感谢您的 sn-p。当我发现这个页面和链接时,我正要写这个。好东西。 这很棒。在我找到这个答案之前,我正要问这个问题。感谢您的帖子 示例测试运行脚本的链接已失效;这是updated link【参考方案2】:

@paluh 的回答需要将不需要的代码添加到非测试文件中,根据我的经验,@carl 的解决方案不适用于使用夹具所需的django.test.TestCase。如果你想使用django.test.TestCase,你需要确保在加载夹具之前调用syncdb。这需要覆盖_pre_setup 方法(将代码放在setUp 方法中是不够的)。我使用我自己的TestCase 版本,它可以让我添加带有测试模型的应用程序。定义如下:

from django.conf import settings
from django.core.management import call_command
from django.db.models import loading
from django import test

class TestCase(test.TestCase):
    apps = ()

    def _pre_setup(self):
        # Add the models to the db.
        self._original_installed_apps = list(settings.INSTALLED_APPS)
        for app in self.apps:
            settings.INSTALLED_APPS.append(app)
        loading.cache.loaded = False
        call_command('syncdb', interactive=False, verbosity=0)
        # Call the original method that does the fixtures etc.
        super(TestCase, self)._pre_setup()

    def _post_teardown(self):
        # Call the original method.
        super(TestCase, self)._post_teardown()
        # Restore the settings.
        settings.INSTALLED_APPS = self._original_installed_apps
        loading.cache.loaded = False

【讨论】:

为了让它与 South 一起工作,我必须将 migrate=False 传递给 call_command。 如果您已将 settings.INSTALLED_APPS 定义为元组(如 django 文档中建议的那样),您首先必须将其转换为列表。否则它工作正常。【参考方案3】:

我分享了我在项目中使用的solution。也许它可以帮助某人。

pip install django-fake-model

创建假模型的两个简单步骤:

1) 在任何文件中定义模型(我通常在测试用例附近的测试文件中定义模型)

from django_fake_model import models as f


class MyFakeModel(f.FakeModel):

    name = models.CharField(max_length=100)

2) 将装饰器 @MyFakeModel.fake_me 添加到您的 TestCase 或测试函数中。

class MyTest(TestCase):

    @MyFakeModel.fake_me
    def test_create_model(self):
        MyFakeModel.objects.create(name='123')
        model = MyFakeModel.objects.get(name='123')
        self.assertEqual(model.name, '123')

此装饰器在每次测试前在您的数据库中创建表,并在测试后删除该表。

您也可以手动创建/删除表:MyFakeModel.create_table() / MyFakeModel.delete_table()

【讨论】:

【参考方案4】:

此解决方案仅适用于早期版本的 django1.7 之前)。您可以轻松检查您的版本:

import django
django.VERSION < (1, 7)

原始回复:

这很奇怪,但我的工作方式非常简单:

    将 tests.py 添加到您要测试的应用中, 在这个文件中只定义测试模型, 在下面放置您的测试代码(doctest 或 TestCase 定义),

下面我放了一些代码,它定义了仅用于测试的 Article 模型(它存在于 someapp/tests.py 中,我可以使用以下命令对其进行测试:./manage.py test someapp ):

class Article(models.Model):
    title = models.CharField(max_length=128)
    description = models.TextField()
    document = DocumentTextField(template=lambda i: i.description)

    def __unicode__(self):
        return self.title

__test__ = "doctest": """
#smuggling model for tests
>>> from .tests import Article

#testing data
>>> by_two = Article.objects.create(title="divisible by two", description="two four six eight")
>>> by_three = Article.objects.create(title="divisible by three", description="three six nine")
>>> by_four = Article.objects.create(title="divisible by four", description="four four eight")

>>> Article.objects.all().search(document='four')
[<Article: divisible by two>, <Article: divisible by four>]
>>> Article.objects.all().search(document='three')
[<Article: divisible by three>]
"""

单元测试也适用于此类模型定义。

【讨论】:

这很棒 - 工作正常(我使用的是 django 1.2.1),这对我来说就像是“正确”的方式。测试模型应作为此应用程序测试的一部分存在。 更新 - 这不适用于灯具,但您可以通过覆盖 _pre_setup 手动调用 syndb(通过 call_command),如 Conley 对此问题的回答中所述【参考方案5】:

我已经找到了一种用于 django 1.7+ 的仅测试模型的方法。

基本思路是,将您的tests 制作成一个应用,并将您的tests 添加到INSTALLED_APPS

这是一个例子:

$ ls common
__init__.py   admin.py      apps.py       fixtures      models.py     pagination.py tests         validators.py views.py

$ ls common/tests
__init__.py        apps.py            models.py          serializers.py     test_filter.py     test_pagination.py test_validators.py views.py

而且我有不同的settings用于不同的目的(参考:splitting up the settings file),即:

settings/default.py: 基本设置文件 settings/production.py:用于生产 settings/development.py: 用于开发 settings/testing.py:用于测试。

而在settings/testing.py,你可以修改INSTALLED_APPS

settings/testing.py:

from default import *

DEBUG = True

INSTALLED_APPS += ['common', 'common.tests']

并确保您为测试应用设置了正确的标签,即,

common/tests/apps.py

from django.apps import AppConfig


class CommonTestsConfig(AppConfig):
    name = 'common.tests'
    label = 'common_tests'

common/tests/__init__.py,设置正确的AppConfig(参考:Django Applications)。

default_app_config = 'common.tests.apps.CommonTestsConfig'

然后,通过

生成db迁移
python manage.py makemigrations --settings=<your_project_name>.settings.testing tests

最后,您可以使用参数--settings=&lt;your_project_name&gt;.settings.testing 运行测试。

如果您使用 py.test,您甚至可以将 pytest.ini 文件与 django 的 manage.py 一起删除。

py.test

[pytest]
DJANGO_SETTINGS_MODULE=kungfu.settings.testing

【讨论】:

不错的方法,似乎是最近 Django 版本中唯一可行的解​​决方案。但值得一提的是 DEBUG=False 用于测试,无论您的 settings.py 你为什么喜欢DEBUG=False in testing.py 不确定我说得对。这不是我的愿望,这是默认的behavior of Django 目前唯一适用于 Django 1.10 的解决方案,谢谢!【参考方案6】:

我选择了一种稍微不同但更耦合的方法来动态创建模型,仅用于测试。

我将所有测试保存在 tests 子目录中,该子目录位于我的 files 应用程序中。 tests 子目录中的 models.py 文件包含我的仅测试模型。耦合部分在这里,我需要将以下内容添加到我的settings.py 文件中:

# check if we are testing right now
TESTING = 'test' in sys.argv

if TESTING:
    # add test packages that have models
    INSTALLED_APPS += ['files.tests',]

我还在我的测试模型中设置了 db_table,因为否则 Django 会创建名为 tests_&lt;model_name&gt; 的表,这可能会导致与另一个应用程序中的其他测试模型发生冲突。这是我的测试模型:

class Recipe(models.Model):

    '''Test-only model to test out thumbnail registration.'''

    dish_image = models.ImageField(upload_to='recipes/')

    class Meta:
        db_table = 'files_tests_recipe'

【讨论】:

这适用于项目,但可能不适用于应用程序。干净的方法。 确实如此。我在想,如果 Django 提供了在应用程序中设置文件的能力,那么这将工作而无需进行项目级别的修改。 嗯,很多应用程序都会考虑项目设置文件。还有这样的选项:github.com/jaredly/django-appsettings 你用的是哪个版本的Django?【参考方案7】:

引用a related answer:

如果您只想为测试定义模型,那么您应该查看 Django ticket #7835 特别是 comment #24 其中一部分 如下:

显然,您可以直接在您的 tests.py 中简单地定义模型。 Syncdb 从不导入 tests.py,因此这些模型不会同步到 正常的数据库,但它们会同步到测试数据库,并且可以 用于测试。

【讨论】:

这似乎在 Django 1.7+ 中变得不那么可靠了,大概是因为处理迁移的方式。 @Sarah:你能详细说明一下吗? Django 1.7+ 没有“syncdb”。我调查已经至少一年了,但是如果我没记错的话, AppConfig.ready() 仅在数据库构建完成并且所有迁移运行后才被调用,并且测试模块甚至在 AppConfig.ready( )。您可能可以使用自定义测试运行程序、settings.py 或 AppConfig 破解某些东西,但我无法让 put-the-models-in-the-tests 的明显变体工作。如果有人有这个工作的 Django 1.7+ 示例,我很高兴看到它。【参考方案8】:

这是我用来执行此操作的模式。

我已经编写了这个方法,用于 TestCase 的子类版本。如下:

@classmethod
def create_models_from_app(cls, app_name):
    """
    Manually create Models (used only for testing) from the specified string app name.
    Models are loaded from the module "<app_name>.models"
    """
    from django.db import connection, DatabaseError
    from django.db.models.loading import load_app

    app = load_app(app_name)
    from django.core.management import sql
    from django.core.management.color import no_style
    sql = sql.sql_create(app, no_style(), connection)
    cursor = connection.cursor()
    for statement in sql:
        try:
            cursor.execute(statement)
        except DatabaseError, excn:
            logger.debug(excn.message)
            pass

然后,我在类似myapp/tests/models.py 的文件中创建了一个特殊的特定于测试的models.py 文件,该文件不包含在INSTALLED_APPS 中。

在我的 setUp 方法中,我调用 create_models_from_app('myapp.tests') 并创建适当的表。

使用这种方法的唯一“问题”是您真的不想在 setUp 运行时创建模型,这就是我捕获 DatabaseError 的原因。我想这个方法的调用可以放在测试文件的顶部,这样会更好一些。

【讨论】:

这个记录器是从哪里导入的?我遇到了这个问题:NameError: global name 'logger' is not defined import logging; logger = logging.getLogger(__name__)【参考方案9】:

结合你的答案,特别是@slacy's,我这样做了:

class TestCase(test.TestCase):
    initiated = False

    @classmethod
    def setUpClass(cls, *args, **kwargs):
        if not TestCase.initiated:
            TestCase.create_models_from_app('myapp.tests')
            TestCase.initiated = True

        super(TestCase, cls).setUpClass(*args, **kwargs)

    @classmethod
    def create_models_from_app(cls, app_name):
        """
        Manually create Models (used only for testing) from the specified string app name.
        Models are loaded from the module "<app_name>.models"
        """
        from django.db import connection, DatabaseError
        from django.db.models.loading import load_app

        app = load_app(app_name)
        from django.core.management import sql
        from django.core.management.color import no_style
        sql = sql.sql_create(app, no_style(), connection)
        cursor = connection.cursor()
        for statement in sql:
            try:
                cursor.execute(statement)
            except DatabaseError, excn:
                logger.debug(excn.message)

有了这个,你不会尝试多次创建数据库表,也不需要更改你的 INSTALLED_APPS。

【讨论】:

【参考方案10】:

如果你正在编写一个可重用的 django-app,为它创建一个最小的测试专用应用

$ django-admin.py startproject test_myapp_project
$ django-admin.py startapp test_myapp

myapptest_myapp 添加到INSTALLED_APPS,在那里创建您的模型,一切顺利!

我已经阅读了所有这些答案以及 django 票 7835,最后我选择了一种完全不同的方法。 我希望我的应用程序(以某种方式扩展 queryset.values() )能够被单独测试;另外,我的包确实包含一些模型,我希望在测试模型和包模型之间有一个清晰的区别。

那时我意识到在包中添加一个非常小的 django 项目更容易! 这也允许更清晰地分离代码恕我直言:

在那里你可以干净利落地定义你的模型,并且你知道当你从那里运行测试时它们会被创建!

如果您不编写独立的、可重用的应用程序,您仍然可以这样:创建一个test_myapp 应用程序,并将其添加到您的 INSTALLED_APPS 中,仅在单独的 settings_test_myapp.py 中!

【讨论】:

【参考方案11】:

有人已经提到了Django ticket #7835,但似乎有一个更新的回复,对于更新的 Django 版本看起来更有希望。具体#42,它提出了不同的TestRunner

from importlib.util import find_spec
import unittest

from django.apps import apps
from django.conf import settings
from django.test.runner import DiscoverRunner


class TestLoader(unittest.TestLoader):
    """ Loader that reports all successful loads to a runner """
    def __init__(self, *args, runner, **kwargs):
        self.runner = runner
        super().__init__(*args, **kwargs)

    def loadTestsFromModule(self, module, pattern=None):
        suite = super().loadTestsFromModule(module, pattern)
        if suite.countTestCases():
            self.runner.register_test_module(module)
        return suite


class RunnerWithTestModels(DiscoverRunner):
    """ Test Runner that will add any test packages with a 'models' module to INSTALLED_APPS.
        Allows test only models to be defined within any package that contains tests.
        All test models should be set with app_label = 'tests'
    """
    def __init__(self, *args, **kwargs):
        self.test_packages = set()
        self.test_loader = TestLoader(runner=self)
        super().__init__(*args, **kwargs)

    def register_test_module(self, module):
        self.test_packages.add(module.__package__)

    def setup_databases(self, **kwargs):
        # Look for test models
        test_apps = set()
        for package in self.test_packages:
            if find_spec('.models', package):
                test_apps.add(package)
        # Add test apps with models to INSTALLED_APPS that aren't already there
        new_installed = settings.INSTALLED_APPS + tuple(ta for ta in test_apps if ta not in settings.INSTALLED_APPS)
        apps.set_installed_apps(new_installed)
        return super().setup_databases(**kwargs)

【讨论】:

以上是关于Django:如何动态创建模型仅用于测试的主要内容,如果未能解决你的问题,请参考以下文章

用户创建的动态数据库 - django

使用 `type` 动态创建 Django 模型

Django:如何创建类别字段/下拉菜单?

Django FormWizard - 如何根据上一步动态创建表单集

向 Django 模型添加动态字段

Django 动态网址