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'
Enter fullscreen mode Exit fullscreen mode
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:
- 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
. - 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 withdumpdata
. 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,
},
),
]
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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):
# ...
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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.
暂无评论内容