loading...

Testing against unmanaged models in Django

vergeev profile image Pavel Vergeev ・4 min read

The problem

My Django application is supposed to read the tables of an already existing database. So the models of the database are generated using inspectdb command. They look like this:

class User(models.Model):
    first_name = models.CharField(max_length=255)
    second_name = models.CharField(max_length=255)
    # other properties

    class Meta:
        managed = False
        db_table = 'user'

The managed = False property tells Django not to create the table as part of migrate command and not to delete it after flush. The catch here is that Django won't create the table during testing either. So when I wanted to test my code against non-production database, I ran into the following issues:

  1. The need to create all of the tables for unmanaged models by hand just to run the tests. Otherwise I was getting django.db.utils.OperationalError: no such table: user.
  2. It was not obvious to me how to generate fixtures for the unmanaged models. If the model is managed, you can run the migrations on the test database, populate the tables with the help of shell command and save it with dumpdata. But the tables of unmanaged models are not created during migrations, there's nothing to populate. Again, I had to create everything by hand.

My goal was to find a way to automate the process, making both testing and collaboration easier. Relevant bits and pieces of are scattered across the Internet. This is an attempt to collect them in one place.

The simple solution

Before the test runner launches the tests, it runs the migrations creating the tables for managed models. The migrations themselves are Python code that creates the models and sets the managed property. It is possible to modify a migration so that it sets managed to True while we are testing and False otherwise for unmanaged models.
To do that, we will create IS_TESTING variable in settings.py and set it to True when we are testing. We will also modify the migration code:

from django.conf import settings

# ...
operations = [
    migrations.CreateModel(
        name='User',
        fields=[
            # ...
        ],
        options={
            'db_table': 'user',
            # 'managed': False,
            'managed': settings.IS_TESTING,
        },
    ),
]

Now the table will be created whenever the migration is run with IS_TESTING = True.
The idea belongs to Kyle Valade, who described it in his blog.
To generate fixtures, the method with shell command described earlier will work.
The downside here is that you have to remember to modify the migration of every unmanaged model.

Creating a custom test runner

A more complex solution is to create a custom test runner that will convert all unmanaged models to managed before running a test and revert the effect afterwards.
We'll put the runner in appname/utils.py:

from django.test.runner import DiscoverRunner


class UnManagedModelTestRunner(DiscoverRunner):

    def setup_test_environment(self, *args, **kwargs):
        from django.apps import apps
        get_models = apps.get_models
        self.unmanaged_models = [m for m in get_models() if not m._meta.managed]
        for m in self.unmanaged_models:
            m._meta.managed = True
        super(UnManagedModelTestRunner, self).setup_test_environment(*args, **kwargs)

    def teardown_test_environment(self, *args, **kwargs):
        super(UnManagedModelTestRunner, self).teardown_test_environment(*args, **kwargs)
        for m in self.unmanaged_models:
            m._meta.managed = False

Now we will tell Django to use this runner by adding TEST_RUNNER = 'app_name.utils.UnManagedModelTestRunner' to our settings.
We are not yet ready because the User model has custom table name user. This is why we need to create the test tables without running migrations. There's a small app for that. It's installed by running pip install and adding it to INSTALLED_APPS. Our tests will work if we run them with -n switch: python manage.py test -n. As a consequence, we will lose the ability to see if any of our migrations are broken (which is probably fine if all of them are generated by Django).

The idea of creating a custom test runner belongs to Tobias McNulty, who posted it in Caktus Group blog. The code from his post had to be updated.

When it comes to maintaining the code, there are complications. First, if we wanted to use some other test runner, we'd have to inherit from it:

from django.test.runner import DiscoverRunner
from django_nose import NoseTestSuiteRunner


class UnManagedModelTestRunner(NoseTestSuiteRunner, DiscoverRunner):
    # ...

Second, even if the django-test-without-migrations app is simple, it doesn't mean it can't be broken by a new version of Django, so we need to be prepared to troubleshoot it.
Third, we have to generate fixtures in an unusual way. The tables of our unmanaged models are only available in setUp() method, so to generate the fixtures we would have to add and dump data in the source code of the test:

import sys

from django.core.management import call_command

# ...

    def setUp(self):
        models.User.objects.create(
            # ...
        )
        sysout = sys.stdout
        sys.stdout = open('fixtures/users.json', 'w')
        call_command('dumpdata', app_label='app_name')
        sys.stdout = sysout

After we ran the code, we can remove it and load the fixture in our tests the usual way.

The common weakness

When the tests run, they will treat the unmanaged models as managed. They won't fail if someone accidentally adds a field to an unmanaged model. The only way I know to get around this is to create the tables by hand.

Final word

This is all I've got on the topic. If you happen to know another solution, I'd be happy to learn about it.

Posted on by:

vergeev profile

Pavel Vergeev

@vergeev

Developer from Moscow with a narrow specialization in Python and Django

Discussion

pic
Editor guide
 

I've found that this only works when a migration is applied to your database when managed=True has been set for all of your models. In this case, we've fixed the problem of Django not being able to load fixtures into the database to run the tests. But what's the point of going through all this trouble when managed=True the whole time anyway?

Does this make sense or am I missing something?

 

I found a much simpler method via Vitor's blog at simpleisbetterthancomplex.com/tips... If you put your unmanaged database models in a different app to your managed ones, you can set

MIGRATION_MODULES = {'my_unmanaged_app': None}

in settings.py. All the relevant tables and columns will be created for testing, but no migrations are necessary.

 

Thanks for taking the time to collect and share this information!

 

Hey, Paveel!

Do you have a link to the repo? I'm trying to figure out which files to put some of the code in.

 

Good day Paul

What django version were you using for this?

I am currently on django 2.0.8 and both solutions did not work. I keep getting django.db.utils.OperationalError: no such table

thanks!

 
 

Just ran into this problem yesterday and wanted to thank you for the simple solution.