How to launch tests for django reusable app?

Can I launch tests for my reusable Django app without incorporating this app into a project?

My app uses some models, so it is necessary to provide (TEST_)DATABASE_* settings. Where should I store them and how should I launch tests?

For a Django project, I can run tests with manage.py test; when I use django-admin.py test with my standalone app, I get:

Error: Settings cannot be imported, because environment variable DJANGO_SETTINGS_MODULE is undefined.

What are the best practises here?


The correct usage of Django (>= 1.4) test runner is as follows:

import django, sys
from django.conf import settings

settings.configure(DEBUG=True,
               DATABASES={
                    'default': {
                        'ENGINE': 'django.db.backends.sqlite3',
                    }
                },
               ROOT_URLCONF='myapp.urls',
               INSTALLED_APPS=('django.contrib.auth',
                              'django.contrib.contenttypes',
                              'django.contrib.sessions',
                              'django.contrib.admin',
                              'myapp',))

try:
    # Django < 1.8
    from django.test.simple import DjangoTestSuiteRunner
    test_runner = DjangoTestSuiteRunner(verbosity=1)
except ImportError:
    # Django >= 1.8
    django.setup()
    from django.test.runner import DiscoverRunner
    test_runner = DiscoverRunner(verbosity=1)

failures = test_runner.run_tests(['myapp'])
if failures:
    sys.exit(failures)

DjangoTestSuiteRunner and DiscoverRunner have mostly compatible interfaces.

For more information you should consult the "Defining a Test Runner" docs:

  • DjangoTestSuiteRunner (Django >=1.4, <1.8)
  • DiscoverRunner (Django >=1.8)

I've ended with such solution (it was inspired by solution found in django-voting):

Create file eg. 'runtests.py' in tests dir containing:

import os, sys
from django.conf import settings

DIRNAME = os.path.dirname(__file__)
settings.configure(DEBUG = True,
                   DATABASE_ENGINE = 'sqlite3',
                   DATABASE_NAME = os.path.join(DIRNAME, 'database.db'),
                   INSTALLED_APPS = ('django.contrib.auth',
                                     'django.contrib.contenttypes',
                                     'django.contrib.sessions',
                                     'django.contrib.admin',
                                     'myapp',
                                     'myapp.tests',))


from django.test.simple import run_tests

failures = run_tests(['myapp',], verbosity=1)
if failures:
    sys.exit(failures)

It allows to run tests by python runtests.py command. It doesn't require installed dependencies (eg. buildout) and it doesn't harm tests run when app is incorporated into bigger project.


For Django 1.7 it's slightly different. Assuming you have the following directory structure for app foo:

foo
|── docs
|── foo
│   ├── __init__.py
│   ├── models.py
│   ├── urls.py
│   └── views.py
└── tests
    ├── foo_models
    │   ├── __init__.py
    │   ├── ...
    │   └── tests.py
    ├── foo_views 
    │   ├── __init__.py
    │   ├── ...
    │   └── tests.py
    ├── runtests.py
    └── urls.py

This is how the Django project itself structures its tests.

You want to run all tests in foo/tests/ with the command:

python3 runtests.py

You also want to be able to run the command from a parent directory of tests, e.g. by Tox or Invoke, just like python3 foo/tests/runtests.py.

The solution I present here is quite reusable, only the app's name foo must be adjusted (and additional apps, if necessary). They can not be installed via modify_settings, because it would miss the database setup.

The following files are needed:

urls.py

"""
This urlconf exists because Django expects ROOT_URLCONF to exist. URLs
should be added within the test folders, and use TestCase.urls to set them.
This helps the tests remain isolated.
"""

urlpatterns = []

runtests.py

#!/usr/bin/env python3
import glob
import os
import sys

import django
from django.conf import settings
from django.core.management import execute_from_command_line


BASE_DIR = os.path.abspath(os.path.dirname(__file__))
sys.path.append(os.path.abspath(os.path.join(BASE_DIR, '..')))

# Unfortunately, apps can not be installed via ``modify_settings``
# decorator, because it would miss the database setup.
CUSTOM_INSTALLED_APPS = (
    'foo',
    'django.contrib.admin',
)

ALWAYS_INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
)

ALWAYS_MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)


settings.configure(
    SECRET_KEY="django_tests_secret_key",
    DEBUG=False,
    TEMPLATE_DEBUG=False,
    ALLOWED_HOSTS=[],
    INSTALLED_APPS=ALWAYS_INSTALLED_APPS + CUSTOM_INSTALLED_APPS,
    MIDDLEWARE_CLASSES=ALWAYS_MIDDLEWARE_CLASSES,
    ROOT_URLCONF='tests.urls',
    DATABASES={
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
        }
    },
    LANGUAGE_CODE='en-us',
    TIME_ZONE='UTC',
    USE_I18N=True,
    USE_L10N=True,
    USE_TZ=True,
    STATIC_URL='/static/',
    # Use a fast hasher to speed up tests.
    PASSWORD_HASHERS=(
        'django.contrib.auth.hashers.MD5PasswordHasher',
    ),
    FIXTURE_DIRS=glob.glob(BASE_DIR + '/' + '*/fixtures/')

)

django.setup()
args = [sys.argv[0], 'test']
# Current module (``tests``) and its submodules.
test_cases = '.'

# Allow accessing test options from the command line.
offset = 1
try:
    sys.argv[1]
except IndexError:
    pass
else:
    option = sys.argv[1].startswith('-')
    if not option:
        test_cases = sys.argv[1]
        offset = 2

args.append(test_cases)
# ``verbosity`` can be overwritten from command line.
args.append('--verbosity=2')
args.extend(sys.argv[offset:])

execute_from_command_line(args)

Some of the settings are optional; they improve speed or a more realistic environment.

The second argument points to the current directory. It makes use of the feature of providing a path to a directory to discover tests below that directory.


For my reusable app(django-moderation) I use buildout. I create example_project, i use it with buildout to run tests on it. I simply put my app inside of settings of example_project.

When i want to install all dependencies used by my project and run tests, i only need to do following:

  • Run: python bootstrap.py
  • Run buildout:

    bin/buildout

  • Run tests for Django 1.1 and Django 1.2:

    bin/test-1.1 bin/test-1.2

Here you can find tutorial how to configure reusable app to use buildout for deployment and tests run: http://jacobian.org/writing/django-apps-with-buildout/

Here you will find example buildout config which i use in my project:

http://github.com/dominno/django-moderation/blob/master//buildout.cfg


In a pure pytest context, to provide just enough Django environment to get tests running for my reusable app without an actual Django project, I needed the following pieces:

pytest.ini:

[pytest]
DJANGO_SETTINGS_MODULE = test_settings
python_files = tests.py test_*.py *_tests.py

test_settings.py:

# You may need more or less than what's shown here - this is a skeleton:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
    }
}

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.messages',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.staticfiles',
    'todo',
)

ROOT_URLCONF = 'base_urls'

TEMPLATES = [
    {
        'DIRS': ['path/to/your/templates'), ],
    }
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

base_urls.py:

"""
This urlconf exists so we can run tests without an actual
Django project (Django expects ROOT_URLCONF to exist.)
It is not used by installed instances of this app.
"""

from django.urls import include, path

urlpatterns = [
    path('foo/', include('myapp.urls')),
]

templates/base.html:

If any of your tests hit actual views, your app's templates will probably be extending the project base.html, so that file must exist. In my case, I just created an empty file templates/base.html.

I can now run pytest -x -v from my standalone reusable app directory, without a Django project.