2016-04-15 28 views
1

Djangoプロジェクトのテストを書くときに、実際にテスト対象のオブジェクトをテストするよりも、データベースレコードを設定するコードが大幅に増えることがあります。現在、関連フィールドを格納するためにテストフィクスチャを使用しようとしていますが、モックオブジェクトを使用して関連するテーブルを模擬して設定する作業が多すぎますか?モックオブジェクトで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() 

答えて

3

それは嘲笑するために正確に何を把握するためにしばらく時間がかかったが、それが可能です。あなたは1対多のフィールドマネージャをモックアウトしますが、クラスクラスではなく、のインスタンスマネージャーではモックアウトする必要があります。ここでは、嘲笑されたマネージャーとのテストの中核です。

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

問題点の1つは、後でテストが失敗する可能性があるということです。コンテキストマネージャを使用してすべての関連フィールドを模擬し、コンテキストを終了するときにそれらを戻す完全な例を次に示します。

# 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()機能も保持しています。模擬テストは、通常のDjangoのテストで実行されたときに今、彼らはすでにセットアップされた通常のDjangoフレームワークを使用し

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 

。モックテストを単独で実行すると、最小限のセットアップが行われます。この設定は、新しいシナリオのテストに役立つように時間が経つにつれて進化したので、latest versionを見てください。非常に便利なツールの1つは、メモリ内に多くのQuerySet機能を提供するdjango-mock-queries libraryです。

我々はtests_mock.pyという名前のファイル内のすべての私たちの模擬テストを置くので、私たちはこのようなすべてのアプリケーションのためのすべての模擬テストを実行することができます。

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

をあなたは模擬テストon GitHub例を見ることができます。

+0

もう1つの選択肢は、あまりにも多くのパッチを当てずに、テストを小さくして、関係を嘲笑して、ポイントにすることです:https://github.com/stphivos/django-mock-queries – fips

+0

ありがとう、それをチェックしてみてください。 –

関連する問題