Django:对抽象模型进行单元测试的最佳方法

时间:2010-11-26 00:07:36

标签: django unit-testing django-models abstract

我需要为抽象基础模型编写一些单元测试,它提供了其他应用程序应该使用的一些基本功能。为了测试目的,有必要定义一个继承它的模型;是否有任何优雅/简单的方法来定义该模型仅用于测试

我看到一些“黑客”使这成为可能,但从未在django文档或其他类似地方看到过“官方”方式。

14 个答案:

答案 0 :(得分:17)

我自己偶然发现了这个功能:您可以在tests.py中继承您的抽象模型并像往常一样进行测试。当您运行'manage.py tests'时,Django不仅会创建一个测试数据库,还会验证&同步您的测试模型。

使用当前的Django trunk(版本1.2)进行测试。

答案 1 :(得分:10)

我也有同样的情况。我最终使用了@dylanboxalot解决方案的版本。在阅读'测试结构概述'后,专门从here获得了额外的详细信息。部分。

每次运行测试时都会调用setUptearDown方法。更好的解决方案是运行“抽象”的创建。在所有测试运行之前,模型一次。为此,您可以实施setUpClassData并实施tearDownClass

class ModelMixinTestCase(TestCase):
    '''
    Base class for tests of model mixins. To use, subclass and specify the
    mixin class variable. A model using the mixin will be made available in
    self.model
    '''
    @classmethod
    def setUpClass(cls):
        # Create a dummy model which extends the mixin
        cls.model = ModelBase('__TestModel__' +
            cls.mixin.__name__, (cls.mixin,),
            {'__module__': cls.mixin.__module__}
        )

        # Create the schema for  our test model
        with connection.schema_editor() as schema_editor:
            schema_editor.create_model(cls.model)
        super(ModelMixinTestCase, cls).setUpClass()

    @classmethod
    def tearDownClass(cls):
        # Delete the schema for the test model
        with connection.schema_editor() as schema_editor:
            schema_editor.delete_model(cls.model)
        super(ModelMixinTestCase, cls).tearDownClass()

可能的实现可能如下所示:

class MyModelTestCase(ModelMixinTestCase):
    mixin = MyModel

    def setUp(self):
        # Runs every time a test is run.
        self.model.objects.create(pk=1)

    def test_my_unit(self):
        # a test
        aModel = self.objects.get(pk=1)
        ...

也许应该将ModelMixinTestCase类添加到Django中? :P

答案 2 :(得分:8)

我最近偶然发现了这个并希望为更新的Django版本(1.9及更高版本)更新它。您可以使用SchemaEditor的create_model而不是过时的sql_create_model

from django.db import connection
from django.db.models.base import ModelBase
from django.test import TestCase


class ModelMixinTestCase(TestCase):
    """
    Base class for tests of model mixins. To use, subclass and specify
    the mixin class variable. A model using the mixin will be made
    available in self.model.
    """

    def setUp(self):
        # Create a dummy model which extends the mixin
        self.model = ModelBase('__TestModel__' + self.mixin.__name__, (self.mixin,), {'__module__': self.mixin.__module__})

        # Create the schema for our test model
        with connection.schema_editor() as schema_editor:
            schema_editor.create_model(self.model)

    def tearDown(self):
        # Delete the schema for the test model
        with connection.schema_editor() as schema_editor:
            schema_editor.delete_model(self.model)

答案 3 :(得分:7)

我认为你要找的是something like this

这是链接的完整代码:

from django.test import TestCase
from django.db import connection
from django.core.management.color import no_style
from django.db.models.base import ModelBase

class ModelMixinTestCase(TestCase):                                         
    """                                                                     
    Base class for tests of model mixins. To use, subclass and specify      
    the mixin class variable. A model using the mixin will be made          
    available in self.model.                                                
    """                                                                     

    def setUp(self):                                                        
        # Create a dummy model which extends the mixin                      
        self.model = ModelBase('__TestModel__'+self.mixin.__name__, (self.mixin,),
            {'__module__': self.mixin.__module__})                          

        # Create the schema for our test model                              
        self._style = no_style()                                            
        sql, _ = connection.creation.sql_create_model(self.model, self._style)

        self._cursor = connection.cursor()                                  
        for statement in sql:                                               
            self._cursor.execute(statement)                                 

    def tearDown(self):                                                     
        # Delete the schema for the test model                              
        sql = connection.creation.sql_destroy_model(self.model, (), self._style)
        for statement in sql:                                               
            self._cursor.execute(statement)                                 

答案 4 :(得分:6)

已针对Django> = 2.0

更新

所以我在使用m4rk4l的答案时遇到了一些问题:一个是在注释之一中出现的“ RuntimeWarning:模型'myapp .__ test__mymodel'已被注册”问题,另一个是由于表已经存在而导致测试失败。

我添加了一些检查以帮助解决这些问题,现在它可以完美地工作了。希望对大家有帮助

from django.db import connection
from django.db.models.base import ModelBase
from django.db.utils import OperationalError
from django.test import TestCase


class AbstractModelMixinTestCase(TestCase):
    """
    Base class for tests of model mixins/abstract models.
    To use, subclass and specify the mixin class variable.
    A model using the mixin will be made available in self.model
    """

@classmethod
def setUpTestData(cls):
    # Create a dummy model which extends the mixin. A RuntimeWarning will
    # occur if the model is registered twice
    if not hasattr(cls, 'model'):
        cls.model = ModelBase(
            '__TestModel__' +
            cls.mixin.__name__, (cls.mixin,),
            {'__module__': cls.mixin.__module__}
        )

    # Create the schema for our test model. If the table already exists,
    # will pass
    try:
        with connection.schema_editor() as schema_editor:
            schema_editor.create_model(cls.model)
        super(AbstractModelMixinTestCase, cls).setUpClass()
    except OperationalError:
        pass

@classmethod
def tearDownClass(self):
    # Delete the schema for the test model. If no table, will pass
    try:
        with connection.schema_editor() as schema_editor:
            schema_editor.delete_model(self.model)
        super(AbstractModelMixinTestCase, self).tearDownClass()
    except OperationalError:
        pass

要使用,请使用与上述相同的方法(现在带有正确的缩进):

class MyModelTestCase(AbstractModelMixinTestCase):
    """Test abstract model."""
    mixin = MyModel

    def setUp(self):
        self.model.objects.create(pk=1)

    def test_a_thing(self):
        mod = self.model.objects.get(pk=1)

答案 5 :(得分:2)

开发一个使用“抽象”模型分发的最小示例应用程序。 为示例应用程序提供测试以证明抽象模型。

答案 6 :(得分:1)

我自己解决了这个问题,我的解决方案就是这个要点django-test-abstract-models

你可以像这样使用它:

1-子类你的django抽象模型

2-写下你的测试用例:

class MyTestCase(AbstractModelTestCase):
    self.models = [MyAbstractModelSubClass, .....]
    # your tests goes here ...

3-如果您未提供self.models属性,则会在当前应用中搜索路径myapp.tests.models.*中的模型

答案 7 :(得分:0)

我认为我可以与您分享我的解决方案,我认为这要简单得多,而且我看不到任何弊端。

示例使用两个抽象类。

from django.db import connection
from django.db.models.base import ModelBase
from mailalert.models import Mailalert_Mixin, MailalertManager_Mixin

class ModelMixinTestCase(TestCase):   

    @classmethod
    def setUpTestData(cls):

        # we define our models "on the fly", based on our mixins
        class Mailalert(Mailalert_Mixin):
            """ For tests purposes only, we fake a Mailalert model """
            pass

        class Profile(MailalertManager_Mixin):
            """ For tests purposes only, we fake a Profile model """
            user = models.OneToOneField(User, on_delete=models.CASCADE, 
                related_name='profile', default=None)

        # then we make those models accessible for later
        cls.Mailalert = Mailalert
        cls.Profile = Profile

        # we create our models "on the fly" in our test db
        with connection.schema_editor() as editor:
            editor.create_model(Profile)
            editor.create_model(Mailalert)

        # now we can create data using our new added models "on the fly"
        cls.user = User.objects.create_user(username='Rick')
        cls.profile_instance = Profile(user=cls.user)
        cls.profile_instance.save()
        cls.mailalert_instance = Mailalert()
        cls.mailalert_instance.save()

# then you can use this ModelMixinTestCase
class Mailalert_TestCase(ModelMixinTestCase):
    def test_method1(self):
       self.assertTrue(self.mailalert_instance.method1())
       # etc

答案 8 :(得分:0)

Django 2.2 中,如果只有一个抽象类要测试,则可以使用以下代码:

from django.db import connection
from django.db import models
from django.db.models.base import ModelBase
from django.db.utils import ProgrammingError
from django.test import TestCase

from yourapp.models import Base  # Base here is the abstract model.


class BaseModelTest(TestCase):
    @classmethod
    def setUpClass(cls):
        # Create dummy model extending Base, a mixin, if we haven't already.
        if not hasattr(cls, '_base_model'):
            cls._base_model = ModelBase(
                'Base',
                ( Base, ),
                { '__module__': Base.__module__ }
            )

            # Create the schema for our base model. If a schema is already
            # create then let's not create another one.
            try:
                with connection.schema_editor() as schema_editor:
                    schema_editor.create_model(cls._base_model)
                super(BaseModelTest, cls).setUpClass()
            except ProgrammingError:
                # NOTE: We get a ProgrammingError since that is what
                #       is being thrown by Postgres. If we were using
                #       MySQL, then we should catch OperationalError
                #       exceptions.
                pass

            cls._test_base = cls._base_model.objects.create()

    @classmethod
    def tearDownClass(cls):
        try:
            with connection.schema_editor() as schema_editor:
                schema_editor.delete_model(cls._base_model)
            super(BaseModelTest, cls).tearDownClass()
        except ProgrammingError:
            # NOTE: We get a ProgrammingError since that is what
            #       is being thrown by Postgres. If we were using
            #       MySQL, then we should catch OperationalError
            #       exceptions.
            pass

此答案只是对DSynergy's answer的调整。一个显着的区别是我们使用setUpClass()而不是setUpTestData()。这种差异很重要,因为在运行其他测试用例时,使用后者会导致InterfaceError(使用PostgreSQL时)或其他数据库中的等效项。至于发生这种情况的原因,在撰写本文时我还不知道。

注意::如果要测试多个抽象类,最好使用其他解决方案。

答案 9 :(得分:0)

我在这里尝试了解决方案,但遇到了类似问题

  

RuntimeWarning:模型'myapp .__ test__mymodel'已经注册

寻找如何使用pytest测试抽象模型也没有成功。我最终想出了一种最适合我的解决方案:

import tempfile

import pytest
from django.db import connection, models
from model_mommy import mommy

from ..models import AbstractModel


@pytest.fixture(scope='module')
def django_db_setup(django_db_setup, django_db_blocker):
    with django_db_blocker.unblock():

        class DummyModel(AbstractModel):
            pass

        class DummyImages(models.Model):
            dummy = models.ForeignKey(
                DummyModel, on_delete=models.CASCADE, related_name='images'
            )
            image = models.ImageField()

        with connection.schema_editor() as schema_editor:
            schema_editor.create_model(DummyModel)
            schema_editor.create_model(DummyImages)


@pytest.fixture
def temporary_image_file():
    image = tempfile.NamedTemporaryFile()
    image.name = 'test.jpg'
    return image.name


@pytest.mark.django_db
def test_fileuploader_model_file_name(temporary_image_file):
    image = mommy.make('core.dummyimages', image=temporary_image_file)
    assert image.file_name == 'test.jpg'


@pytest.mark.django_db
def test_fileuploader_model_file_mime_type(temporary_image_file):
    image = mommy.make('core.dummyimages', image=temporary_image_file)
    assert image.file_mime_type == 'image/jpeg'

如您所见,我定义了一个继承自Abstractmodel的Class,并将其添加为固定装置。 现在有了模型妈咪的灵活性,我可以创建一个DummyImages对象,它也会自动为我创建一个DummyModel!

或者,我可以通过不包含外键来简化示例,但是它很好地展示了pytest和model mommy组合的灵活性。

答案 10 :(得分:0)

这是django 3.0中使用Postgres的有效解决方案。它可以测试任意数量的抽象模型,并保持与异物有关的任何完整性。

from typing import Union
from django.test import TestCase
from django.db import connection
from django.db.models.base import ModelBase
from django.db.utils import ProgrammingError

# Category and Product are abstract models
from someApp.someModule.models import Category, Product, Vendor, Invoice

class MyModelsTestBase(TestCase):
    @classmethod
    def setUpTestData(cls):
        # keep track of registered fake models
        # to avoid RuntimeWarning when creating
        # abstract models again in the class
        cls.fake_models_registry = {}

    def setUp(self):
        self.fake_models = []

    def tearDown(self):
        try:
            with connection.schema_editor(atomic=True) as schema_editor:
                for model in self.fake_models:
                    schema_editor.delete_model(model)
        except ProgrammingError:
            pass

    def create_abstract_models(self, models: Union[list, tuple]):
        """
        param models: list/tuple of abstract model class
        """
        # by keeping model names same as abstract model names
        # we are able to maintain any foreign key relationship
        model_names = [model.__name__ for model in models]
        modules = [model.__module__ for model in models]
        for idx, model_name in enumerate(model_names):
            # if we already have a ModelBase registered
            # avoid re-registering.
            registry_key = f'{modules[idx]}.{model_name}'
            model_base = self.fake_models_registry.get(registry_key)
            if model_base is not None:
                self.fake_models.append(model_base)
                continue

            # we do not have this model registered
            # so register it and track it in our
            # cls.fake_models_registry            
            self.fake_models.append(
                ModelBase(
                    model_name,
                    (models[idx],),
                    {'__module__': modules[idx]}
                )
            )
            self.fake_models_registry[registry_key] = self.fake_models[idx]

        errors = []
        # atomic=True allows creating multiple models in the db
        with connection.schema_editor(atomic=True) as schema_editor:
            try:
                for model in self.fake_models:
                    schema_editor.create_model(model)
             except ProgrammingError as e:
                 errors.append(e)
                 pass
        return errors

    def test_create_abstract_models(self):
        abstract_models = (Category, Product)
        errors = self.create_abstract_models(abstract_models)
        self.assertEqual(len(errors), 0)

        category_model_class, product_model_class = self.fake_models

        # and use them like any other concrete model class:
        category = category_model_class.objects.create(name='Pet Supplies')
        product = product_model_class.objects.create(
            name='Dog Food', category_id=category.id
        )


答案 11 :(得分:0)

在阅读完以上所有答案后,我发现了在PostgreSQL 3.1.2和PostgreSQL 12.4数据库中对我有用的解决方案。

from django.db import connection
from django.db.utils import ProgrammingError
from django.test import TestCase


class AbstractModelTestCase(TestCase):
    """
    Base class for tests of model mixins. To use, subclass and specify the
    mixin class variable. A model using the mixin will be made available in
    self.model
    """

    @classmethod
    def setUpClass(cls):
        if not hasattr(cls, "model"):
            super(AbstractModelTestCase, cls).setUpClass()
        else:
            # Create the schema for our test model. If the table already exists, will pass
            try:
                with connection.schema_editor() as schema_editor:
                    schema_editor.create_model(cls.model)
                super(AbstractModelTestCase, cls).setUpClass()
            except ProgrammingError:
                pass

    @classmethod
    def tearDownClass(cls):
        if hasattr(cls, "model"):
            # Delete the schema for the test model
            with connection.schema_editor() as schema_editor:
                schema_editor.delete_model(cls.model)
        super(AbstractModelTestCase, cls).tearDownClass()

它也摆脱了烦人的RuntimeWarning: Model 'xxx' was already registered警告。

答案 12 :(得分:0)

Maikhoepfel's answer 是正确的,其他大多数看起来都不必要地复杂。我想提供进一步的说明,因为其他更复杂的答案似乎很受欢迎。

project/
├─ app1/
├─ app2/
│  ├─ tests/
│  │  ├─ __init__.py
│  │  ├─ models.py
│  │  ├─ test_models.py
│  ├─ __init__.py
│  ├─ apps.py
│  ├─ models.py

鉴于上述项目结构,app2.tests.models 中继承自 app2.models.YourAbstractModel 的模型将可用于任何测试(例如 app2.tests.test_models),而无需运行迁移。

这方面的例子可以在 Django test source code 中看到。

答案 13 :(得分:-4)

测试抽象类并不太有用,因为派生类可以覆盖其方法。其他应用程序负责根据您的抽象类测试它们的类。