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

Posted

技术标签:

【中文标题】Django:对抽象模型进行单元测试的最佳方法【英文标题】:Django: Best way to unit-test an abstract model 【发布时间】:2011-05-15 23:08:45 【问题描述】:

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

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

【问题讨论】:

【参考方案1】:

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

【讨论】:

我想喜欢这个解决方案,但是在数据库中创建表有缺点。【参考方案2】:

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

【讨论】:

你的抽象基础模型可以有非常丰富的功能,并且你不想测试它的所有方法是否正常工作。而且你不想做一次。否则,其他人每次从您的抽象模型派生时都必须测试相同的代码。这些应用只需要测试它们覆盖的方法,而且只需要它们。【参考方案3】:

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

使用当前的 Django 主干(1.2 版)对其进行了测试。

【讨论】:

只需添加一件事:如果您的测试在一个文件夹中,而不仅仅是在 tests.py 中(而我的从不适合一个文件),那么您需要有 Meta 内部类,带有你的 app_label 集(就像你拆分了你的 models.py 文件一样)。 听起来不错...但在我的情况下不起作用。将继承的类放在 models.py 中正常工作,但将类放在 tests.py 中不会获得“用于测试的同步数据库”创建表。需要明确的是:我只想要这张表进行测试。使用 Django 1.2.3。有任何想法吗?注意:使用 django-nose 测试运行器。也许它的行为不同(现在正在调查)。 更新:确实在 django-nose 运行器中会发生错误,但使用标准 django 测试运行器可以正常工作。 github.com/jbalogh/django-nose/issues/15 存在 django-nose 问题,其中包含一些背景知识和一些修复。 使用 django 1.6.0【参考方案4】:

我想你要找的是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)                                 

【讨论】:

这是一个死链接 但是,如何使用呢?我的意思是,很好,我扩展......现在呢? 在示例中,您只需将self.mixin 属性设置为您要测试的任何抽象类。然后 setUp 将为您的抽象类 (self.model) 创建一个子类并将其添加到数据库中。然后,您可以通过在self.model 上测试它们来向ModelMixinTestCase 添加实际测试抽象类功能的方法。 这里是上面代码的导入。 from django.test import TestCasefrom django.db import connectionfrom django.core.management.color import no_stylefrom django.db.models.base import ModelBase 使用示例代码需要以下内容:1) 扩展ModelMixingTestCase,2) 覆盖setUp 并执行此操作:self.mixin = MyClass 3) 像这样调用super (Python 2.7):@ 987654336@ 其中TestMyClass 是我的类的名称,继承自ModelMixinTestCase【参考方案5】:

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

你可以这样使用它:

1- 继承你的 django 抽象模型

2- 像这样编写你的测试用例:

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

3- 如果您没有提供 self.models 属性,它将在当前应用程序中搜索路径 myapp.tests.models.* 中的模型

【讨论】:

它在 Django 1.10 上是否适合您,如果可以,您是否有可能发布一个小的完整工作示例,会很棒吗?【参考方案6】:

我最近偶然发现了这一点,并想为更新的 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)

【讨论】:

当我的测试类中的第二个测试方法运行时,我得到django/db/models/base.py:325: RuntimeWarning: Model 'myapp.__test__mymodel' was already registered。 tearDown 方法不应该防止这种情况发生吗?【参考方案7】:

我也有同样的情况。我最终使用了@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

【讨论】:

为什么是setUpTestData 而不是setUpClass? Django 文档指出setUpTestData 用于提供初始数据。 你是对的!我换个方法。我在这个答案中找到了更多信息:link 使用这个我得到一个错误:AttributeError: type object 'MyModelTestCase' has no attribute 'cls_atomics'。将setUpClass 还原为setUpTestData 可以解决问题。所以我的评论引入了错误。因此,我很抱歉,文档有些误导,链接的 SO 答案支持我的反对意见。 我也认为,在 Django 中应该有一个用于测试抽象模型类的集成解决方案。这应该是公认的答案。这是最优雅的解决方案。不幸的是,我只能投票一次。 在拆解时引发错误:E django.db.utils.NotSupportedError: SQLite schema editor cannot be used while foreign key constraint checks are enabled. Make sure to disable them before entering a transaction.atomic() context because SQLite does not support disabling them in the middle of a multi-statement transaction.。此线程中的解决方案:***.com/questions/57583985/…【参考方案8】:

为 Django >=2.0 更新

所以我在使用 m4rk4l 的回答时遇到了一些问题:一个是在其中一个 cmets 中出现的“RuntimeWarning: Model '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)

【讨论】:

我认为OperationalError 应该是ProgrammingError 我不这么认为? OperationalError:对于不受程序员控制的事情会引发此异常。例如,意外断开连接、内存分配错误等,选定的数据库不存在。 ProgrammingError:此异常由编程错误引发。例如找不到表、mysql 语法错误、指定的参数数量错误等。取自:[thepythonguru.com/handling-errors/] 所以数据库实际上在这里有所作为:Mysql: OperationalError Postgresql: ProgrammingError【参考方案9】:

我想我可以与您分享我的解决方案,在我看来这要简单得多,而且我没有看到任何缺点。

以使用两个抽象类为例。

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

【讨论】:

【参考方案10】:

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 时)或其他数据库中的等价物。至于发生这种情况的原因,我在撰写本文时不知道。

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

【讨论】:

请注意:如果您使用的是多数据库设置,您可以使用from django.db import connections(注意连接,而不是连接)并按照with connections['other'].schema_editor() as schema_editor: ... 的方式进行操作。【参考方案11】:

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

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,并将其添加为夹具。 现在有了model mommy的灵活性,我可以创建一个DummyImages对象,它也会自动为我创建一个DummyModel!

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

【讨论】:

【参考方案12】:

这是一个在 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
        )


【讨论】:

【参考方案13】:

在阅读了上述所有答案后,我找到了一个适合我的解决方案,在 Django 3.1.1 和 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 警告。

【讨论】:

【参考方案14】:

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看到。

【讨论】:

以上是关于Django:对抽象模型进行单元测试的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

如何对抽象类进行单元测试

使用 Spring Security ACL 注释对方法进行单元测试的最佳方法是啥?

对生成随机输出的代码进行单元测试的最佳方法是啥?

在 Laravel 中对控制器进行单元测试而不测试路由的最佳方法是啥

对 ASP.NET 2.0 网页进行单元测试的最佳方法是啥? [关闭]

使用 PHPUnit 对具有多种用户类型的网站进行单元测试的最佳方法