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】:此解决方案仅适用于早期版本的 django
(1.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=<your_project_name>.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_<model_name>
的表,这可能会导致与另一个应用程序中的其他测试模型发生冲突。这是我的测试模型:
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 definedimport 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
将myapp
和test_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:如何动态创建模型仅用于测试的主要内容,如果未能解决你的问题,请参考以下文章