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!