71

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
  • 15,777
  • 9
  • 59
  • 98
  • This question is pretty involved because it’s trying to set up a model that can be referenced from `settings`. If you just want to create a standalone model in some tests, look at https://stackoverflow.com/questions/59000963/how-to-dynamicaly-create-models-in-tests – andrewdotn Jan 04 '23 at 23:45

12 Answers12

55

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.

Carl Meyer
  • 122,012
  • 20
  • 106
  • 116
  • 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. – muhuk Feb 03 '09 at 10:41
  • 1
    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". – Carl Meyer Feb 03 '09 at 15:44
  • Carl, thanks for the snippet. I was about to go write this when I found this page and the link. Good stuff. – cethegeek Nov 03 '09 at 20:36
  • this is great. I was just about to ask this question before I found this answer. Thanks for the post – Timmy O'Mahony Jun 14 '11 at 11:56
  • 4
    The link for the example test-running script is dead; here's the [updated link](https://github.com/carljm/django-model-utils/blob/master/runtests.py) – Aron Griffis Jul 18 '12 at 20:49
  • This approach seems not to work at least in Django 1.9.1: There is no django.db.models.loading anymore – Fallen Flint Nov 15 '16 at 09:50
  • 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. – Carl Meyer Nov 15 '16 at 15:41
  • 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. – Routhinator Nov 05 '18 at 13:37
  • @Routhinator, maybe the [django docs](https://docs.djangoproject.com/en/dev/topics/testing/advanced/#using-the-django-test-runner-to-test-reusable-applications) can help. They provide an example with dedicated `tests/models.py` and `tests/test_settings.py`. – djvg Nov 19 '18 at 11:08
  • They do, and I've mirrored their setup, but it doesn't work as expected. – Routhinator Nov 21 '18 at 16:08
  • 4
    @AronGriffis The updated link is also dead. – Amir Shabani Mar 08 '22 at 10:54
20

@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
djvg
  • 11,722
  • 5
  • 72
  • 103
Conley Owens
  • 8,691
  • 5
  • 30
  • 43
  • 6
    To get this to work with [South](http://south.aeracode.org/), I had to pass `migrate=False` to call_command. – Vebjorn Ljosa Nov 26 '10 at 17:21
  • 2
    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. – marue Apr 25 '12 at 14:01
18

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

Kirill Ermolov
  • 780
  • 6
  • 19
14

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
Xiao Hanyu
  • 1,402
  • 16
  • 11
11

Quoting from a related answer:

If you want models defined for testing only then you should check out Django ticket #7835 in particular comment #24 part of which is given below:

Apparently you can simply define models directly in your tests.py. Syncdb never imports tests.py, so those models won't get synced to the normal db, but they will get synced to the test database, and can be used in tests.

Community
  • 1
  • 1
joeharrie
  • 111
  • 1
  • 3
  • 1
    This seems to have become less reliable in Django 1.7+, presumably because of the way migrations are being handled. – Sarah Messer Apr 30 '15 at 18:47
  • @Sarah: can you elaborate on that? – Raffi Oct 10 '16 at 13:08
  • 3
    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. – Sarah Messer Oct 10 '16 at 22:03
11

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.

paluh
  • 2,171
  • 20
  • 14
  • 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 Jul 26 '10 at 13:24
  • 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 – adamnfish Jul 26 '10 at 15:07
10

I chose a slightly different, albeit more coupled, approach to dynamically creating models just for testing.

I keep all my tests in a tests subdirectory that lives in my files app. The models.py file in the tests subdirectory contains my test-only models. The coupled part comes in here, where I need to add the following to my settings.py file:

# check if we are testing right now
TESTING = 'test' in sys.argv

if TESTING:
    # add test packages that have models
    INSTALLED_APPS += ['files.tests',]

I also set db_table in my test model, because otherwise Django would have created the table with the name tests_<model_name>, which may have caused a conflict with other test models in another app. Here's my my test model:

class Recipe(models.Model):

    '''Test-only model to test out thumbnail registration.'''

    dish_image = models.ImageField(upload_to='recipes/')

    class Meta:
        db_table = 'files_tests_recipe'
Jashugan
  • 517
  • 4
  • 13
  • This would work well for a project, but probably not for an app. Clean approach though. – muhuk Jan 04 '12 at 01:07
  • 1
    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. – Jashugan Jan 04 '12 at 01:11
  • Well, lots of apps take into account project settings files. There's also the option of something like this: https://github.com/jaredly/django-appsettings – Mike Shultz Sep 17 '13 at 16:34
  • Which version of Django do you use ? – Zulu Dec 21 '14 at 16:47
4

Here's the pattern that I'm using to do this.

I've written this method that I use on a subclassed version of TestCase. It goes as follows:

@classmethod
def create_models_from_app(cls, app_name):
    """
    Manually create Models (used only for testing) from the specified string app name.
    Models are loaded from the module "<app_name>.models"
    """
    from django.db import connection, DatabaseError
    from django.db.models.loading import load_app

    app = load_app(app_name)
    from django.core.management import sql
    from django.core.management.color import no_style
    sql = sql.sql_create(app, no_style(), connection)
    cursor = connection.cursor()
    for statement in sql:
        try:
            cursor.execute(statement)
        except DatabaseError, excn:
            logger.debug(excn.message)
            pass

Then, I create a special test-specific models.py file in something like myapp/tests/models.py that's not included in INSTALLED_APPS.

In my setUp method, I call create_models_from_app('myapp.tests') and it creates the proper tables.

The only "gotcha" with this approach is that you don't really want to create the models ever time setUp runs, which is why I catch DatabaseError. I guess the call to this method could go at the top of the test file and that would work a little better.

slacy
  • 11,397
  • 8
  • 56
  • 61
4

Combining your answers, specially @slacy's, I did this:

class TestCase(test.TestCase):
    initiated = False

    @classmethod
    def setUpClass(cls, *args, **kwargs):
        if not TestCase.initiated:
            TestCase.create_models_from_app('myapp.tests')
            TestCase.initiated = True

        super(TestCase, cls).setUpClass(*args, **kwargs)

    @classmethod
    def create_models_from_app(cls, app_name):
        """
        Manually create Models (used only for testing) from the specified string app name.
        Models are loaded from the module "<app_name>.models"
        """
        from django.db import connection, DatabaseError
        from django.db.models.loading import load_app

        app = load_app(app_name)
        from django.core.management import sql
        from django.core.management.color import no_style
        sql = sql.sql_create(app, no_style(), connection)
        cursor = connection.cursor()
        for statement in sql:
            try:
                cursor.execute(statement)
            except DatabaseError, excn:
                logger.debug(excn.message)

With this, you don't try to create db tables more than once, and you don't need to change your INSTALLED_APPS.

zVictor
  • 3,610
  • 3
  • 41
  • 56
1

If you are writing a reusable django-app, create a minimal test-dedicated app for it!

$ django-admin.py startproject test_myapp_project
$ django-admin.py startapp test_myapp

add both myapp and test_myapp to the INSTALLED_APPS, create your models there and it's good to go!

I have gone through all these answers as well as django ticket 7835, and I finally went for a totally different approach. I wanted my app (somehow extending queryset.values() ) to be able to be tested in isolation; also, my package does include some models and I wanted a clean distinction between test models and package ones.

That's when I realized it was easier to add a very small django project in the package! This also allows a much cleaner separation of code IMHO:

In there you can cleanly and without any hack define your models, and you know they will be created when you run your tests from in there!

If you are not writing an independent, reusable app you can still go this way: create a test_myapp app, and add it to your INSTALLED_APPS only in a separate settings_test_myapp.py!

Stefano
  • 18,083
  • 13
  • 64
  • 79
1

You can use @isolate_apps decorator.

This is documented in the Django developers documentation. You can read here: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/#tips-for-writing-tests

Basically, here is how you can use it:

from django.test.utils import isolate_apps
from django.test import TestCase

class DummyModelTestCase(TestCase):

    @isolate_apps("label")
    def test_dummy_model(self):

        # create your dynamic model
        class DummyModel(models.Model):
            attrib = "value"
            class Meta:
                app_label = "label"

        model = DummyModel()

        # your assertions
        self.assertEqual(model.attrib, "value")       

Also, here is an example of isolate_apps in wild: https://github.com/laymonage/django-jsonfield-backport/blob/2072fb39b6681f2bf8741e033702920b59238941/tests/test_invalid_models.py#L12

Mohsen Hassani
  • 101
  • 1
  • 5
0

Someone already mentioned Django ticket #7835, but there appears to be a more recent reply that looks much more promising for more recent versions of Django. Specifically #42, which proposes a different TestRunner:

from importlib.util import find_spec
import unittest

from django.apps import apps
from django.conf import settings
from django.test.runner import DiscoverRunner


class TestLoader(unittest.TestLoader):
    """ Loader that reports all successful loads to a runner """
    def __init__(self, *args, runner, **kwargs):
        self.runner = runner
        super().__init__(*args, **kwargs)

    def loadTestsFromModule(self, module, pattern=None):
        suite = super().loadTestsFromModule(module, pattern)
        if suite.countTestCases():
            self.runner.register_test_module(module)
        return suite


class RunnerWithTestModels(DiscoverRunner):
    """ Test Runner that will add any test packages with a 'models' module to INSTALLED_APPS.
        Allows test only models to be defined within any package that contains tests.
        All test models should be set with app_label = 'tests'
    """
    def __init__(self, *args, **kwargs):
        self.test_packages = set()
        self.test_loader = TestLoader(runner=self)
        super().__init__(*args, **kwargs)

    def register_test_module(self, module):
        self.test_packages.add(module.__package__)

    def setup_databases(self, **kwargs):
        # Look for test models
        test_apps = set()
        for package in self.test_packages:
            if find_spec('.models', package):
                test_apps.add(package)
        # Add test apps with models to INSTALLED_APPS that aren't already there
        new_installed = settings.INSTALLED_APPS + tuple(ta for ta in test_apps if ta not in settings.INSTALLED_APPS)
        apps.set_installed_apps(new_installed)
        return super().setup_databases(**kwargs)
André Fratelli
  • 5,920
  • 7
  • 46
  • 87