Django: Best way to unit-test an abstract model

后端 未结 13 1283
一个人的身影
一个人的身影 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 12:59

    I came to this problem my self and my solution is on this gist django-test-abstract-models

    you can use it like this:

    1- subclass your django abstract models

    2- write your test case like this:

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

    3- if you didn't provide self.models attribute it will search the current app for models in the path myapp.tests.models.*

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

    Here is a working solution in django 3.0 with Postgres. It allows testing any number of abstract models and also maintains any integrity related to foreign objects.

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

    I think what you are looking for is something like this.

    This is the full code from the link:

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

    I have the same situation as well. I ended up using a version of @dylanboxalot solution. Got extra details from here specifically after reading 'Test structure overview' section.

    The setUp and the tearDown methods are called each time a tests is run. A better solution is to run the creation of the 'abstract' model once, before all the tests are run. To do so, you can implement the setUpClassData and also implement the 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()
    

    A possible implementation may look like this:

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

    Maybe ModelMixinTestCase class should be added to Django? :P

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

    I tried solutions here but ran into issues like

    RuntimeWarning: Model 'myapp.__test__mymodel' was already registered

    Looking up how to test abstract models with pytest wasn't any successful either. I eventually came up with this solution that works perfectly for me:

    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'
    

    As you can see, I define a Class that inherits from the Abstractmodel, and add it as a fixture. Now with the flexibility of model mommy, I can create a DummyImages object, and it will automatically create a DummyModel for me too!

    Alternatively, I could've made the example simple by not including foreign keys, but it demonstrates the flexibility of pytest and model mommy in combination quite well.

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

    Testing an abstract class is not too useful, as a derived class can override its methods. The other applications are responsible for testing their classes based on your abstract class.

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