代码之家  ›  专栏  ›  技术社区  ›  Bernhard Vallant

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

  •  20
  • Bernhard Vallant  · 技术社区  · 14 年前

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

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

    9 回复  |  直到 14 年前
        1
  •  20
  •   jrobichaud    6 年前

    我刚刚偶然发现了这个特性:您可以从中的抽象模型继承测试.py像往常一样测试。当你跑的时候管理.pyDjango不仅创建一个测试数据库,还验证和同步测试模型。

        2
  •  10
  •   cezar Dennis Kioko    7 年前

    我也有同样的情况。最后我使用了@dylanboxalot解决方案。从那里得到了更多的细节 here 特别是在阅读“测试结构概述”部分之后。

    这个 setUp tearDown 方法在每次运行测试时调用。更好的解决方案是在运行所有测试之前,运行一次“抽象”模型的创建。为此,可以实现 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

        3
  •  10
  •   DSynergy    6 年前

    所以我在使用m4rk4l的答案时遇到了一些问题:一个是“RuntimeWarning:Model”myapp.\uUu test_UuMyModel“已注册”问题,另一个是测试失败,因为该表已经存在。

    我添加了一些检查来帮助解决这些问题,现在它完美地工作了。我希望这能帮助人们

    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)
    
        4
  •  8
  •   dylanboxalot    8 年前

    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)
    
        5
  •  7
  •   simlmx    8 年前

    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)                                 
    
        6
  •  2
  •   hash1baby    14 年前

    开发一个最小的示例应用程序,并与您的“抽象”模型一起分发。

        7
  •  2
  •   MYaser    11 年前

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

    你可以这样使用它:

    2-编写测试用例如下:

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

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

        8
  •  1
  •   Sean Francis N. Ballais Raindrop7    5 年前

    迪亚戈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
  •   KrazyMax    6 年前

    我想我可以和你分享我的解决方案,在我看来,这个方案简单得多,我看不出任何缺点。

    这个例子使用了两个抽象类。

    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
  •  0
  •   Viktor Johansson    5 年前

    我在这里尝试过解决方案,但遇到了一些问题

    查找如何用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'
    

    如您所见,我定义了一个继承自抽象模型的类,并将其作为fixture添加。 现在有了mommy模型的灵活性,我可以创建一个DummyImages对象,它也会自动为我创建一个DummyModel!

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

        11
  •  0
  •   Zul Qasar    5 年前

    下面是一个使用Postgres在django3.0中工作的解决方案。它允许测试任意数量的抽象模型,还可以维护任何与外来对象相关的完整性。

    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
            )
    
    
    
        12
  •  0
  •   Dharman Aman Gojariya    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

        13
  •  -4
  •   Stephen Curial    14 年前

    测试抽象类并不是太有用,因为派生类可以重写其方法。其他应用程序负责基于抽象类测试它们的类。