2016-04-15 2 views
1

Часто, когда я пишу тесты для своего проекта Django, мне нужно написать намного больше кода для настройки записей базы данных, чем я, чтобы фактически протестировать тестируемый объект. В настоящее время я пытаюсь использовать тестовые приборы для хранения связанных полей, но могу ли я использовать макетные объекты для издевательства над связанными таблицами, которые требуют много работы для настройки?Упростите тест Django с помощью mock-объектов

Вот тривиальный пример. Я хочу проверить, что объект 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() 

ответ

3

потребовалось некоторое время, чтобы выяснить точно, что издеваться, но это возможно. Вы издеваетесь над полевым менеджером «один ко многим», но вы должны издеваться над классом , а не на примере . Вот ядро ​​теста с менеджером.

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, но мы обнаружили, что тесты Джанго все медленнее, как мы добавили больше и больше миграций. Чтобы пропустить создание тестовой базы данных, когда мы запускаем проверенные тесты, мы добавили модуль 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.

+0

Еще один вариант, который вам может быть интересен - для насмешливых отношений без слишком большого количества исправлений и выполнения тестов меньше и по существу: https://github.com/stphivos/django-mock-queries – fips

+0

Спасибо, @fips, проверить это. –

Смежные вопросы