使用模拟对象简化Django测试设置 [英] Simplify Django test set up with mock objects

查看:47
本文介绍了使用模拟对象简化Django测试设置的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

通常,当我为Django项目编写测试时,与实际测试被测对象相比,我必须编写更多的代码来建立数据库记录.目前,我尝试使用测试装置来存储相关字段,但是我可以使用模拟对象来模拟需要花费大量工作来建立的相关表吗?

Often when I'm writing tests for my Django project, I have to write a lot more code to set up database records than I do to actually test the object under test. Currently, I try to use test fixtures to store the related fields, but could I use mock objects to mock out the related tables that take so much work to set up?

这是一个简单的例子.我想测试一个 Person 对象是否会根据其健康状况 spawn()子代.

Here's a trivial example. I want to test that a Person object will spawn() children according to its health.

在这种情况下,一个人的城市是必填字段,因此即使该城市与 spawn()方法完全无关,我也必须先建立一个城市才能创建一个人..我如何简化此测试以不要求创建城市?(在一个典型的示例中,不相关但需要设置的记录可能是数十或数百条记录,而不仅仅是一条记录.)

In this case, a person's city is a required field, so I have to set up a city before I can create a person, even though the city is completely irrelevant to the spawn() method. How could I simplify this test to not require creating a city? (In a typical example, the irrelevant but required set up could be tens or hundreds of records instead of just one.)

# 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()

推荐答案

花了一段时间才弄清楚要模拟的内容,但这是可能的.您可以模拟一对多的字段管理器,但是必须在 class 而不是在 instance 上模拟它.这是带有模拟管理员的测试的核心.

It took a while to figure out exactly what to mock, but it is possible. You mock out the one-to-many field manager, but you have to mock it out on the class, not on the instance. Here's the core of the test with a mocked out manager.

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')

一个问题是以后的测试可能会失败,因为您离开了经理.这是一个带有上下文管理器的完整示例,用于模拟所有相关字段,然后在您离开上下文时将它们放回原处.

One problem with that is that later tests will probably fail because you left the manager mocked out. Here's a full example with a context manager to mock out all the related fields, and then put them back when you leave the context.

# 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()函数.

You can mix mocked tests in with your regular Django tests, but we found that the Django tests got slower as we added more and more migrations. To skip the test database creation when we run the mocked tests, we added a mock_setup module. It has to be imported before any Django models, and it does a minimal set up of the Django framework before the tests run. It also holds the mock_relations() function.

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框架.当模拟测试单独运行时,它们会进行最少的设置.该设置随着时间的推移而不断发展,以帮助测试新的场景,因此请查看最新版本.一个非常有用的工具是 django-mock-queries 在内存中提供了许多 QuerySet 功能.

Now when the mock tests are run with the regular Django tests, they use the regular Django framework that's already set up. When the mock tests are run on their own, they do a minimal set up. That set up has evolved over time to help test new scenarios, so look at the latest version. One very useful tool is the django-mock-queries library that provides a lot of the QuerySet features in memory.

我们将所有模拟测试放在名为 tests_mock.py 的文件中,因此我们可以为所有此类应用程序运行所有模拟测试:

We put all our mock tests in files named tests_mock.py, so we can run all the mock tests for all the apps like this:

python -m unittest discover -p 'tests_mock.py'

您可以在GitHub上看到上的示例模拟测试.

You can see an example mock test on GitHub.

这篇关于使用模拟对象简化Django测试设置的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆