Unit testing abstract models in Django and factoryboy

I had to test an abstract model recently and I also wanted to take advantage of factoryboy while I was at it. However, a slight problem arose – I could not use it due to the way factories are defined: FACTORY_FOR requires the model class to be defined and present in the database upfront. This is not the case since I was not dealing with a concrete models and tables for abstract models are not created.

The first question that I was asked was: Why on earth would you test an abstract model? To be clear – I don’t want to test the model itself – I want to test its methods and I want to do it before they are potentially overriden.

The question that followed was: Can’t you just create a concrete model that would inherit from your abstract model, place it next to it and test it instead? No, I don’t want to pollute non-test code with test code. And since I effectively only want to mock the model, creating a wrapper app would be an overkill. You will notice that I am indeed constructing a concrete model, but it is performed outside the app models.py file so it will never get added to my regular, non-test database.

The code

The following solution is derived from Conley Owens custom TestCase to remove unwanted code from a non-test file. The code assumes you are using 2.0.0+ version of factoryboy. However, if you are still using one of the earlier versions then I think you could probably get away with it by changing factory.django.DjangoModelFactory to factory.Factory.

# project/utils/test_utils.py
from django.test import TestCase
 
 
class ExtraModelsTestCase(TestCase):
    # Change to True if using South for DB migrations and migrating during tests
    south_migrate = False
    def _pre_setup(self):
        # Add the models to the db.
        self._original_installed_apps = list(settings.INSTALLED_APPS)
        # Update installed apps with test apps
        settings.INSTALLED_APPS += self.apps
        loading.cache.loaded = False
        call_command('syncdb', interactive=False, migrate=south_migrate, verbosity=0)
        # Call the original method that does the fixtures etc.
        super(TestCase, self)._pre_setup()
        # Prepare Factories for any of the extra models
        try:
            for extra_model in self.extra_models:
                # create Factory class for extra_model accessible under {{ extra_model }}Factory name
                factory_class = '%sFactory' % extra_model.__name__
                cls = type(factory_class, (factory.django.DjangoModelFactory,), dict(FACTORY_FOR=extra_model))
                setattr(self, factory_class, cls)
        except AttributeError:
            pass
 
    def _post_teardown(self):
        # Call the original method.
        super(TestCase, self)._post_teardown()
        # Restore the settings.
        settings.INSTALLED_APPS = self._original_installed_apps
        loading.cache.loaded = False

Usage

For the sake of brevity our TestModel is created outside the test case. If you insisted, you could always create the model dynamically using python’s built-in type() function. All you have to do is to inherit your test case from ExtraModelsTestCase and define a list/tuple of apps and a list/tuple of extra_models that you want factories created for. I hope the code below is self-explanatory enough for you to comprehend. Let me know if something is not clear.

What you already have

# project/utils/models.py
from django.db import models
 
 
class AbstractModel(models.Model):
    class Meta:
        abstract = True
 
    def test_method(self):
        return True

What you are missing

# project/utils/tests.py
from project.utils.models import AbstractModel
from project.utils.test_utils import ExtraModelsTestCase
 
 
class TestModel(AbstractModel):
    pass
 
 
class TestModelTestCase(ExtraModelsTestCase):
    apps = ('project.utils',)
    extra_models = (TestModel,)
 
    def test_abstract(self):
        self.instance = self.TestModelFactory.create()
        self.assertTrue(self.instance.test_method())

The TestModelTestCase will automagically receive a factory for TestModel in a TestModelFactory attribute that can be used like a regular factory. Enjoy or let me know if you have more elegant approach to this problem!

Leave a Reply