Fix weird exceptions when running Django tests

Michal Bock
3 min readMay 14, 2018

--

If you are reading this article then you probably got around to writing tests for your Django project and when running them using python manage.py test you encountered an exception like this

django.contrib.admin.sites.AlreadyRegistered: The model Country is already registered

or this

RuntimeError: Model class app.world.models.country.Country doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS.

Why does this happen and how to solve it?

The reason why you get these exceptions is that you structured your admin and/or models as packages and imported the individual admin and/or model classes in their respective __init__.py files.

This might seem like a reasonable thing to do, but it confuses the test runner when it’s looking for tests. The structure makes it load the admin files multiple times what results in the AlreadyRegistered exception. However, your admin files has to be read, so that your models are registered. One way to avoid getting this exception is to import the files in the ready function of your app config instead of the __init__.py file. An example is shown below.

from django.apps import AppConfig


class WorldConfig(AppConfig):
name = 'world'

def ready(self):
from world.admin import countries
from world.admin import continents

What is the problem with the models?

Importing the model classes in the __init__.py file of the models package confuses the test runner when reading this file as for some reason it can’t identify to which app these models belong. This is why you get the RuntimeError. If you remove the imports from this file everything will work just fine. Migrations for new models will be created as long as you use them somewhere. The only disadvantage is that you will have to specify the full path when importing the models. I.e. you will have to import models like shown on the second line below instead of the shorter version on the first line.

from world.models import Country
from world.models.countries import Country

Are there other ways to solve these problems?

The simplest solution is to keep the models and admin as simple modules and potentially split your app into multiple smaller apps when the files become too large. However, sometimes this might not be the best option.

In that case the easiest way to avoid these errors is to explicitly specify the names of the apps you want to test. For example if you have two apps called "world" and "people" you can run tests for them like this

python my_app/manage.py test world people

The biggest issue with this approach is that it won’t work if you run the tests from the directory where your manage.py file is. Having to explicitly list all of your apps is also not ideal.

Is there a good solution which doesn’t force me to rewrite my code?

The solution I originally came up with is to use django-nose package to run the tests. You can tell Django to use Nose test runner by adding the following line to your settings.

TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'

However, this alone doesn’t solve the issue, as Nose test runner looks for the tests the same way as the default Django one. The advantage of using Nose is that we can use any of the available Nose extensions, so we just need to find one that allows us to tell Nose where not to look. A tiny package called nose-pattern-exclude allows you to do exactly that. Once installed running the tests using the following command will work just fine.

python manage.py test --exclude-path=../*/admin --exclude-path=../*/models

If you want to avoid having to specify the exclude paths every time you run the tests, you can set these as default arguments for Nose in your settings by adding the following.

NOSE_ARGS = [
'--exclude-path=../*/admin',
'--exclude-path=../*/models',
]

With these settings you can now run the tests with python manage.py test and everything will work as you would expect. I know this solution is not optimal as it requires installing two additional packages for testing. However, I think it might be a better option than having to change your code just to make the test runner work. Please let me know in the comments if you have better solutions or what do think about mine.

--

--

Michal Bock

Senior Software Engineer at Deliveroo. Oxford graduate in Mathematics and Computer Science. Working with Golang, gRPC, Kubernetes, Python and Django.