Don*_*kby 6 python django unit-testing
通常,当我为 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()
Run Code Online (Sandbox Code Playgroud)
我花了一段时间才弄清楚到底要嘲笑什么,但这是可能的。您模拟了一对多字段管理器,但您必须在类上模拟它,而不是在实例上。这是模拟经理测试的核心。
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')
Run Code Online (Sandbox Code Playgroud)
这样做的一个问题是,以后的测试可能会失败,因为你让经理被模拟了。这是一个完整的示例,其中使用上下文管理器来模拟所有相关字段,然后在离开上下文时将它们放回原处。
# 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()
Run Code Online (Sandbox Code Playgroud)
您可以将模拟测试与常规 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
Run Code Online (Sandbox Code Playgroud)
现在,当模拟测试与常规 Django 测试一起运行时,它们使用已设置的常规 Django 框架。当模拟测试单独运行时,他们会进行最小的设置。该设置随着时间的推移而不断发展,以帮助测试新场景,因此请查看最新版本。一个非常有用的工具是提供许多内存功能的django-mock-queries库。QuerySet
我们将所有模拟测试放在名为 的文件中tests_mock.py,这样我们就可以为所有应用程序运行所有模拟测试,如下所示:
python -m unittest discover -p 'tests_mock.py'
Run Code Online (Sandbox Code Playgroud)
您可以在 GitHub 上查看示例模拟测试。
| 归档时间: |
|
| 查看次数: |
11851 次 |
| 最近记录: |