Django: Best way to unit-test an abstract model

后端 未结 13 1273
一个人的身影
一个人的身影 2020-12-24 12:28

I need to write some unit tests for an abstract base model, that provides some basic functionality that should be used by other apps. It it would be necessary to define a mo

相关标签:
13条回答
  • 2020-12-24 13:11

    Updated for Django >=2.0

    So I was running into a few problems using m4rk4l's answer: one being the 'RuntimeWarning: Model 'myapp.__test__mymodel' was already registered' issue brought up in one of the comments, another being tests failing because the table already exists.

    I've added a few checks to help solve these issues and now it works flawlessly. I hope this helps people

    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
    

    To use, implement the same way as above (now with the correcting indentation):

    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)
    
    0 讨论(0)
  • 2020-12-24 13:16

    Just stumbled across this feature myself: You can just inherit from your abstract model in tests.py and test that as usual. When you run 'manage.py tests', Django not only creates a test database, but also validates & syncs your test models.

    Tested it with current Django trunk (version 1.2).

    0 讨论(0)
  • 2020-12-24 13:18

    I stumbled across this recently and wanted to update it for newer Django versions (1.9 and later) You can use the SchemaEditor's create_model instead of the outdated 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)
    
    0 讨论(0)
  • 2020-12-24 13:18

    In Django 2.2, if you only have one abstract class to test, you can use the following:

    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
    

    This answer is only a tweaking of DSynergy's answer. One notable difference is that we are using setUpClass() instead of setUpTestData(). This difference is important since using the latter will result in InterfaceError (when using PostgreSQL) or the equivalent in other databases when the other test cases are run. As to the reason why this happens, I do not know at the time of writing.

    NOTE: If you have more than one abstract class to test, it is better to use the other solutions.

    0 讨论(0)
  • 2020-12-24 13:19

    I thought I could share with you my solution, which is in my opinion much simpler and I do not see any cons.

    Example goes for using two abstract classes.

    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
    
    0 讨论(0)
  • 2020-12-24 13:20

    Having read through all the answers above, I found out a solution that worked for me, in Django 3.1.1 with PostgreSQL 12.4 database.

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

    It also gets rid of the annoying RuntimeWarning: Model 'xxx' was already registered warning.

    0 讨论(0)
提交回复
热议问题