2016-04-15 16 views
1

Oft, wenn ich Tests für mein Django-Projekt schreibe, muss ich viel mehr Code schreiben, um Datenbank-Datensätze einzurichten, als ich tatsächlich das zu testende Objekt zu testen. Momentan versuche ich, Test-Fixtures zu verwenden, um die zugehörigen Felder zu speichern, aber könnte ich Mock-Objekte verwenden, um die zugehörigen Tabellen, die so viel Arbeit in Anspruch nehmen, auszuspionieren?Simplify Django Test eingerichtet mit Mock-Objekten

Hier ist ein triviales Beispiel. Ich möchte testen, ob ein Person Objekt spawn() Kinder nach seiner Gesundheit ist.

In diesem Fall ist die Stadt einer Person ein Pflichtfeld, also muss ich eine Stadt einrichten, bevor ich eine Person erstellen kann, obwohl die Stadt für die spawn() Methode völlig irrelevant ist. Wie kann ich diesen Test vereinfachen, um keine Stadt zu erstellen? (In einem typischen Beispiel, die irrelevant aber erforderlich einrichten könnten Dutzende oder Hunderte von Datensätzen statt nur einer sein.)

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

Antwort

3

Es dauerte eine Weile, genau herauszufinden, was zu verspotten, aber es ist möglich. Sie verspotten den one-to-many-Feldmanager, aber Sie müssen es auf der Klasse, nicht auf der Instanz ausspionieren. Hier ist der Kern des Tests mit einem verspotteten Manager.

Ein Problem damit ist, dass spätere Tests wahrscheinlich fehlschlagen werden, weil Sie den Manager spotteten. Im Folgenden finden Sie ein vollständiges Beispiel mit einem Kontextmanager, der alle verwandten Felder ausgibt und sie dann wieder zurückgibt, wenn Sie den Kontext verlassen.

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

Sie mischen Tests mit Ihrem normalen Django Tests verspottet, aber wir fanden, dass die Django-Tests wurde langsamer, als wir immer mehr Migrationen hinzugefügt. Um die Testdatenbankerstellung zu überspringen, wenn wir die mocked Tests ausführen, fügten wir ein mock_setup-Modul hinzu. Es muss vor allen Django-Modellen importiert werden, und das Django-Framework muss vor dem Test minimal eingerichtet werden. Es hält auch die mock_relations() Funktion.

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 

Nun, wenn die Mock Tests mit den regelmäßigen Tests Django ausgeführt werden, verwenden sie die regelmäßige Django Framework, das oben bereits festgelegt ist. Wenn die Mock-Tests von selbst ausgeführt werden, sind sie minimal eingerichtet. Dieser Aufbau hat sich im Laufe der Zeit weiterentwickelt, um neue Szenarien zu testen. Schauen Sie sich die latest version an. Ein sehr nützliches Werkzeug ist die django-mock-queries library, die viele der QuerySet Funktionen im Speicher bietet.

Wir setzen alle unsere Mock Tests in Dateien mit dem Namen tests_mock.py, also können wir alle Mock Tests für alle Anwendungen wie diese laufen:

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

Sie ein Beispiel on GitHub Mock Test sehen.

+0

Eine andere Option, die Sie interessieren könnten - für die Verspottung Beziehungen ohne zu viel Patching und machen Tests kleiner und auf den Punkt: https://github.com/stphivos/django-mock-eries – fips

+0

Danke, @fips, ich ' Ich werde das überprüfen. –