Django: How to create a model dynamically just for testing

27,807

Solution 1

You can put your tests in a tests/ subdirectory of the app (rather than a tests.py file), and include a tests/models.py with the test-only models.

Then provide a test-running script (example) that includes your tests/ "app" in INSTALLED_APPS. (This doesn't work when running app tests from a real project, which won't have the tests app in INSTALLED_APPS, but I rarely find it useful to run reusable app tests from a project, and Django 1.6+ doesn't by default.)

(NOTE: The alternative dynamic method described below only works in Django 1.1+ if your test case subclasses TransactionTestCase - which slows down your tests significantly - and no longer works at all in Django 1.7+. It's left here only for historical interest; don't use it.)

At the beginning of your tests (i.e. in a setUp method, or at the beginning of a set of doctests), you can dynamically add "myapp.tests" to the INSTALLED_APPS setting, and then do this:

from django.core.management import call_command
from django.db.models import loading
loading.cache.loaded = False
call_command('syncdb', verbosity=0)

Then at the end of your tests, you should clean up by restoring the old version of INSTALLED_APPS and clearing the app cache again.

This class encapsulates the pattern so it doesn't clutter up your test code quite as much.

Solution 2

@paluh's answer requires adding unwanted code to a non-test file and in my experience, @carl's solution does not work with django.test.TestCase which is needed to use fixtures. If you want to use django.test.TestCase, you need to make sure you call syncdb before the fixtures get loaded. This requires overriding the _pre_setup method (putting the code in the setUp method is not sufficient). I use my own version of TestCase that lets me add apps with test models. It is defined as follows:

from django.conf import settings
from django.core.management import call_command
from django.db.models import loading
from django import test

class TestCase(test.TestCase):
    apps = ()

    def _pre_setup(self):
        # Add the models to the db.
        self._original_installed_apps = list(settings.INSTALLED_APPS)
        for app in self.apps:
            settings.INSTALLED_APPS.append(app)
        loading.cache.loaded = False
        call_command('syncdb', interactive=False, verbosity=0)
        # Call the original method that does the fixtures etc.
        super(TestCase, self)._pre_setup()

    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

Solution 3

I shared my solution that I use in my projects. Maybe it helps someone.

pip install django-fake-model

Two simple steps to create fake model:

1) Define model in any file (I usualy define model in test file near a test case)

from django_fake_model import models as f


class MyFakeModel(f.FakeModel):

    name = models.CharField(max_length=100)

2) Add decorator @MyFakeModel.fake_me to your TestCase or to test function.

class MyTest(TestCase):

    @MyFakeModel.fake_me
    def test_create_model(self):
        MyFakeModel.objects.create(name='123')
        model = MyFakeModel.objects.get(name='123')
        self.assertEqual(model.name, '123')

This decorator creates table in your database before each test and remove the table after test.

Also you may create/delete table manually: MyFakeModel.create_table() / MyFakeModel.delete_table()

Solution 4

I've figured out a way for test-only models for django 1.7+.

The basic idea is, make your tests an app, and add your tests to INSTALLED_APPS.

Here's an example:

$ ls common
__init__.py   admin.py      apps.py       fixtures      models.py     pagination.py tests         validators.py views.py

$ ls common/tests
__init__.py        apps.py            models.py          serializers.py     test_filter.py     test_pagination.py test_validators.py views.py

And I have different settings for different purposes(ref: splitting up the settings file), namely:

  • settings/default.py: base settings file
  • settings/production.py: for production
  • settings/development.py: for development
  • settings/testing.py: for testing.

And in settings/testing.py, you can modify INSTALLED_APPS:

settings/testing.py:

from default import *

DEBUG = True

INSTALLED_APPS += ['common', 'common.tests']

And make sure that you have set a proper label for your tests app, namely,

common/tests/apps.py

from django.apps import AppConfig


class CommonTestsConfig(AppConfig):
    name = 'common.tests'
    label = 'common_tests'

common/tests/__init__.py, set up proper AppConfig(ref: Django Applications).

default_app_config = 'common.tests.apps.CommonTestsConfig'

Then, generate db migration by

python manage.py makemigrations --settings=<your_project_name>.settings.testing tests

Finally, you can run your test with param --settings=<your_project_name>.settings.testing.

If you use py.test, you can even drop a pytest.ini file along with django's manage.py.

py.test

[pytest]
DJANGO_SETTINGS_MODULE=kungfu.settings.testing

Solution 5

This solution works only for earlier versions of django (before 1.7). You can check your version easily:

import django
django.VERSION < (1, 7)

Original response:

It's quite strange but form me works very simple pattern:

  1. add tests.py to app which you are going to test,
  2. in this file just define testing models,
  3. below put your testing code (doctest or TestCase definition),

Below I've put some code which defines Article model which is needed only for tests (it exists in someapp/tests.py and I can test it just with: ./manage.py test someapp ):

class Article(models.Model):
    title = models.CharField(max_length=128)
    description = models.TextField()
    document = DocumentTextField(template=lambda i: i.description)

    def __unicode__(self):
        return self.title

__test__ = {"doctest": """
#smuggling model for tests
>>> from .tests import Article

#testing data
>>> by_two = Article.objects.create(title="divisible by two", description="two four six eight")
>>> by_three = Article.objects.create(title="divisible by three", description="three six nine")
>>> by_four = Article.objects.create(title="divisible by four", description="four four eight")

>>> Article.objects.all().search(document='four')
[<Article: divisible by two>, <Article: divisible by four>]
>>> Article.objects.all().search(document='three')
[<Article: divisible by three>]
"""}

Unit tests also working with such model definition.

Share:
27,807
muhuk
Author by

muhuk

3D Artist &amp; Programmer https://www.muhuk.com

Updated on August 24, 2021

Comments

  • muhuk
    muhuk over 2 years

    I have a Django app that requires a settings attribute in the form of:

    RELATED_MODELS = ('appname1.modelname1.attribute1',
                      'appname1.modelname2.attribute2', 
                      'appname2.modelname3.attribute3', ...)
    

    Then hooks their post_save signal to update some other fixed model depending on the attributeN defined.

    I would like to test this behaviour and tests should work even if this app is the only one in the project (except for its own dependencies, no other wrapper app need to be installed). How can I create and attach/register/activate mock models just for the test database? (or is it possible at all?)

    Solutions that allow me to use test fixtures would be great.

  • muhuk
    muhuk over 15 years
    That's a clean and powerful snipplet (I guess it's yours). Creating a whole app at first seemed like too much just for a mock model. But now I think it represents real world usage best from a unit testing perspective. Thanks.
  • Carl Meyer
    Carl Meyer over 15 years
    Yeah, I don't know what's best, but this works for me. "Creating a whole app" seems like a lot less of a big deal when you realize that all it really means is "create a models.py file".
  • cethegeek
    cethegeek over 14 years
    Carl, thanks for the snippet. I was about to go write this when I found this page and the link. Good stuff.
  • adamnfish
    adamnfish almost 14 years
    This is great - works fine (I'm using django 1.2.1) and this feels like the 'right' way to do it to me. The test model should exist as part of the tests for this application.
  • adamnfish
    adamnfish almost 14 years
    Update - this doesn't work for fixtures but you can call syndb manually (via call_command) by overriding _pre_setup as described in Conley's answer to this question
  • VIGNESH
    VIGNESH over 13 years
    To get this to work with South, I had to pass migrate=False to call_command.
  • Timmy O'Mahony
    Timmy O'Mahony almost 13 years
    this is great. I was just about to ask this question before I found this answer. Thanks for the post
  • muhuk
    muhuk over 12 years
    This would work well for a project, but probably not for an app. Clean approach though.
  • Jashugan
    Jashugan over 12 years
    That's true. I was thinking that if Django shipped with the ability to have setting files in apps, then this would work without having to make project level modifications.
  • marue
    marue about 12 years
    If you have defined settings.INSTALLED_APPS as a tuple (like proposed in the django docs) you first have to convert it to a list as well. Otherwise it works fine.
  • Aron Griffis
    Aron Griffis almost 12 years
    The link for the example test-running script is dead; here's the updated link
  • zVictor
    zVictor almost 12 years
    Where is this logger imported from? I am having this issue: NameError: global name 'logger' is not defined
  • slacy
    slacy over 11 years
    import logging; logger = logging.getLogger(__name__)
  • Mike Shultz
    Mike Shultz over 10 years
    Well, lots of apps take into account project settings files. There's also the option of something like this: github.com/jaredly/django-appsettings
  • Zulu
    Zulu over 9 years
    Which version of Django do you use ?
  • Sarah Messer
    Sarah Messer about 9 years
    This seems to have become less reliable in Django 1.7+, presumably because of the way migrations are being handled.
  • Raffi
    Raffi over 7 years
    @Sarah: can you elaborate on that?
  • Sarah Messer
    Sarah Messer over 7 years
    Django 1.7+ doesn't have "syncdb". It's been at least a year since I investigated, but if I recall correctly, AppConfig.ready() is only called after the DB has been built and all migrations run, and the tests module isn't even loaded until after AppConfig.ready(). You might be able to hack something with a custom test runner, settings.py, or AppConfig, but I was not able to get obvious variants of put-the-models-in-the-tests to work. If someone has a Django 1.7+ example of this working, I'd be happy to see it.
  • Fallen Flint
    Fallen Flint over 7 years
    This approach seems not to work at least in Django 1.9.1: There is no django.db.models.loading anymore
  • Fallen Flint
    Fallen Flint over 7 years
    Nice approach, seems to be the only working solution in recent Django versions. But it's worth mention that DEBUG=False for testing regardless your settings.py
  • Carl Meyer
    Carl Meyer over 7 years
    Yes. The answer specifically says the dynamic approach doesn't work in Django 1.7+ and is left here only for historical interest. The right approach is outlined in the first paragraph, and still works fine.
  • Xiao Hanyu
    Xiao Hanyu over 7 years
    Why would you like DEBUG=False in testing.py?
  • Fallen Flint
    Fallen Flint over 7 years
    not sure I got you right. It's not my desire, it's the default behavior of Django
  • Alex Paramonov
    Alex Paramonov about 7 years
    The only solution at the moment that works in Django 1.10, thanks!
  • Routhinator
    Routhinator over 5 years
    I cannot get the approach outlined in the first paragraph to work in Django 2.0+ - Seems as though this entire answer is now deprecated.
  • djvg
    djvg over 5 years
    @Routhinator, maybe the django docs can help. They provide an example with dedicated tests/models.py and tests/test_settings.py.
  • Routhinator
    Routhinator over 5 years
    They do, and I've mirrored their setup, but it doesn't work as expected.
  • Amir Shabani
    Amir Shabani about 2 years
    @AronGriffis The updated link is also dead.