通常,当我为 Django 项目编写测试时,我必须编写比实际测试被测对象更多的代码来设置数据库记录。目前,我尝试使用测试装置来存储相关字段,但是我可以使用模拟对象来模拟需要大量工作来设置的相关表吗?
这是一个简单的例子。我想测试 Person
对象是否会根据其健康状况 spawn()
子对象。
在这种情况下,一个人的城市是必填字段,因此我必须先设置一个城市,然后才能创建一个人,即使该城市与 spawn()
方法完全无关。我怎样才能简化这个测试而不需要创建一个城市? (在典型示例中,不相关但必需的设置可能是数十或数百条记录,而不仅仅是一条。)
# Tested with Django 1.9.2
import sys
import django
from django.apps import apps
from django.apps.config import AppConfig
from django.conf import settings
from django.db import connections, models, DEFAULT_DB_ALIAS
from django.db.models.base import ModelBase
NAME = 'udjango'
def main():
setup()
class City(models.Model):
name = models.CharField(max_length=100)
class Person(models.Model):
name = models.CharField(max_length=50)
city = models.ForeignKey(City, related_name='residents')
health = models.IntegerField()
def spawn(self):
for i in range(self.health):
self.children.create(name='Child{}'.format(i))
class Child(models.Model):
parent = models.ForeignKey(Person, related_name='children')
name = models.CharField(max_length=255)
syncdb(City)
syncdb(Person)
syncdb(Child)
# A typical unit test would start here.
# The set up is irrelevant to the test, but required by the database.
city = City.objects.create(name='Vancouver')
# Actual test
dad = Person.objects.create(name='Dad', health=2, city=city)
dad.spawn()
# Validation
children = dad.children.all()
num_children = len(children)
assert num_children == 2, num_children
name2 = children[1].name
assert name2 == 'Child1', name2
# End of typical unit test.
print('Done.')
def setup():
DB_FILE = NAME + '.db'
with open(DB_FILE, 'w'):
pass # wipe the database
settings.configure(
DEBUG=True,
DATABASES={
DEFAULT_DB_ALIAS: {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': DB_FILE}},
LOGGING={'version': 1,
'disable_existing_loggers': False,
'formatters': {
'debug': {
'format': '%(asctime)s[%(levelname)s]'
'%(name)s.%(funcName)s(): %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S'}},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'debug'}},
'root': {
'handlers': ['console'],
'level': 'WARN'},
'loggers': {
"django.db": {"level": "WARN"}}})
app_config = AppConfig(NAME, sys.modules['__main__'])
apps.populate([app_config])
django.setup()
original_new_func = ModelBase.__new__
@staticmethod
def patched_new(cls, name, bases, attrs):
if 'Meta' not in attrs:
class Meta:
app_label = NAME
attrs['Meta'] = Meta
return original_new_func(cls, name, bases, attrs)
ModelBase.__new__ = patched_new
def syncdb(model):
""" Standard syncdb expects models to be in reliable locations.
Based on https://github.com/django/django/blob/1.9.3
/django/core/management/commands/migrate.py#L285
"""
connection = connections[DEFAULT_DB_ALIAS]
with connection.schema_editor() as editor:
editor.create_model(model)
main()
最佳答案
花了一段时间才弄清楚到底要模拟什么,但这是可能的。您模拟了一对多字段管理器,但必须在类上模拟它,而不是在实例上模拟它。这是使用模拟管理器进行的测试的核心。
Person.children = Mock()
dad = Person(health=2)
dad.spawn()
num_children = len(Person.children.create.mock_calls)
assert num_children == 2, num_children
Person.children.create.assert_called_with(name='Child1')
其中的一个问题是,以后的测试可能会失败,因为你让经理被模拟了。这是一个完整的示例,其中使用上下文管理器来模拟所有相关字段,然后在离开上下文时将它们放回原处。
# Tested with Django 1.9.2
from contextlib import contextmanager
from mock import Mock
import sys
import django
from django.apps import apps
from django.apps.config import AppConfig
from django.conf import settings
from django.db import connections, models, DEFAULT_DB_ALIAS
from django.db.models.base import ModelBase
NAME = 'udjango'
def main():
setup()
class City(models.Model):
name = models.CharField(max_length=100)
class Person(models.Model):
name = models.CharField(max_length=50)
city = models.ForeignKey(City, related_name='residents')
health = models.IntegerField()
def spawn(self):
for i in range(self.health):
self.children.create(name='Child{}'.format(i))
class Child(models.Model):
parent = models.ForeignKey(Person, related_name='children')
name = models.CharField(max_length=255)
syncdb(City)
syncdb(Person)
syncdb(Child)
# A typical unit test would start here.
# The irrelevant set up of a city and name is no longer required.
with mock_relations(Person):
dad = Person(health=2)
dad.spawn()
# Validation
num_children = len(Person.children.create.mock_calls)
assert num_children == 2, num_children
Person.children.create.assert_called_with(name='Child1')
# End of typical unit test.
print('Done.')
@contextmanager
def mock_relations(model):
model_name = model._meta.object_name
model.old_relations = {}
model.old_objects = model.objects
try:
for related_object in model._meta.related_objects:
name = related_object.name
model.old_relations[name] = getattr(model, name)
setattr(model, name, Mock(name='{}.{}'.format(model_name, name)))
setattr(model, 'objects', Mock(name=model_name + '.objects'))
yield
finally:
model.objects = model.old_objects
for name, relation in model.old_relations.iteritems():
setattr(model, name, relation)
del model.old_objects
del model.old_relations
def setup():
DB_FILE = NAME + '.db'
with open(DB_FILE, 'w'):
pass # wipe the database
settings.configure(
DEBUG=True,
DATABASES={
DEFAULT_DB_ALIAS: {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': DB_FILE}},
LOGGING={'version': 1,
'disable_existing_loggers': False,
'formatters': {
'debug': {
'format': '%(asctime)s[%(levelname)s]'
'%(name)s.%(funcName)s(): %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S'}},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'debug'}},
'root': {
'handlers': ['console'],
'level': 'WARN'},
'loggers': {
"django.db": {"level": "WARN"}}})
app_config = AppConfig(NAME, sys.modules['__main__'])
apps.populate([app_config])
django.setup()
original_new_func = ModelBase.__new__
@staticmethod
def patched_new(cls, name, bases, attrs):
if 'Meta' not in attrs:
class Meta:
app_label = NAME
attrs['Meta'] = Meta
return original_new_func(cls, name, bases, attrs)
ModelBase.__new__ = patched_new
def syncdb(model):
""" Standard syncdb expects models to be in reliable locations.
Based on https://github.com/django/django/blob/1.9.3
/django/core/management/commands/migrate.py#L285
"""
connection = connections[DEFAULT_DB_ALIAS]
with connection.schema_editor() as editor:
editor.create_model(model)
main()
您可以将模拟测试与常规 Django 测试混合在一起,但我们发现随着我们添加越来越多的迁移,Django 测试变得更慢。为了在运行模拟测试时跳过测试数据库创建,我们添加了一个 mock_setup
模块。它必须在任何 Django 模型之前导入,并且在测试运行之前对 Django 框架进行最小化设置。它还包含 mock_relations()
函数。
from contextlib import contextmanager
from mock import Mock
import os
import django
from django.apps import apps
from django.db import connections
from django.conf import settings
if not apps.ready:
# Do the Django set up when running as a stand-alone unit test.
# That's why this module has to be imported before any Django models.
if 'DJANGO_SETTINGS_MODULE' not in os.environ:
os.environ['DJANGO_SETTINGS_MODULE'] = 'kive.settings'
settings.LOGGING['handlers']['console']['level'] = 'CRITICAL'
django.setup()
# Disable database access, these are pure unit tests.
db = connections.databases['default']
db['PASSWORD'] = '****'
db['USER'] = '**Database disabled for unit tests**'
@contextmanager
def mock_relations(*models):
""" Mock all related field managers to make pure unit tests possible.
with mock_relations(Dataset):
dataset = Dataset()
check = dataset.content_checks.create() # returns mock object
"""
try:
for model in models:
model_name = model._meta.object_name
model.old_relations = {}
model.old_objects = model.objects
for related_object in model._meta.related_objects:
name = related_object.name
model.old_relations[name] = getattr(model, name)
setattr(model, name, Mock(name='{}.{}'.format(model_name, name)))
model.objects = Mock(name=model_name + '.objects')
yield
finally:
for model in models:
old_objects = getattr(model, 'old_objects', None)
if old_objects is not None:
model.objects = old_objects
del model.old_objects
old_relations = getattr(model, 'old_relations', None)
if old_relations is not None:
for name, relation in old_relations.iteritems():
setattr(model, name, relation)
del model.old_relations
现在,当模拟测试与常规 Django 测试一起运行时,它们使用已设置的常规 Django 框架。当模拟测试单独运行时,他们会进行最小的设置。该设置随着时间的推移而不断发展,以帮助测试新场景,因此请查看 latest version 。 django-mock-queries
library 是一个非常有用的工具。它在内存中提供了许多 QuerySet
功能。
我们将所有模拟测试放在名为 tests_mock.py
的文件中,这样我们就可以为所有应用程序运行所有模拟测试,如下所示:
python -m unittest discover -p 'tests_mock.py'
您可以看到一个模拟测试示例 on GitHub .
关于python - 使用模拟对象简化 Django 测试设置,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36658010/