Test a Reusable Django Application for Support of Multiple Django Releases with Tox and TravisCI
Instructions for adding Tox and TravisCI to an existing reusable Django application in order to test for backwards and forwards compatibility with multiple Django and Python releases.
There are three main steps to this:
- Adding support for testing a reusable Django application against a single version of Python and a single version of Django by using a demo Django project and Setup Test.
- Automating the local testing of a reusable Django application against multiple versions of Python and multiple versions of Django in virtual environments using Tox.
- Automating the remote testing of a reusable Django application against multiple containerized versions of Python and multiple versions of Django in virtual environments using Tox on the TravisCI platform.
Assumptions
- a reusable Django application is already created and hosted on GitHub
- a TravisCI account has been created and associated with the GitHub user (TravisCI is free for OpenSource projects)
Create a local copy of the reusable Django application if it doesn't already exist; clone the repo and create a virtual environment to work in:
$ git clone __repo_url__
$ cd $_
$ mkvirtualenv -a . __env_name__
Calling the Test Suite from the Setup script
Since this is a reusable Django application, it needs to be tested in a Django project. Update the project setup script to call the Django test suite. Eric Holser explains this in depth in a still mostly relevant post from 2009. The Django documentation also includes an example.
As Eric explains, running the test suite without the Django management command relies on setting a test_suite
in the setup()
method call of the project's setup.py
file that calls the test runner. That should look something like:
...
setup(
...
test_suite="runtests.runtests",
...
)
test_suite
is calling a function called runtests
in the runtests
module which in turn is going to call the test runner. To make that module, create a file called runtests.py
also in the root of the project:
$ touch runtests.py
Edit that to look like this:
import os
import sys
import django
from django.conf import settings
from django.test.utils import get_runner
def runtests():
os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.test_settings'
django.setup()
TestRunner = get_runner(settings)
test_runner = TestRunner()
failures = test_runner.run_tests(["tests"])
sys.exit(bool(failures))
if __name__ == '__main__':
runtests()
The runtests()
function calls the test runner for a demo Django project called tests
. Now, create that project. This could be done by installing the Django package and creating the project using django-admin createproject tests
but that's probably overkill. Instead, create the project from scratch. At the minimum that requires a tests
directory in the root of the reusable application, an empty __init__.py
to identify it a module, an empty models.py
so tests can run, a settings file named test_settings.py
, a urls.py
file and a test suite (an empty tests.py
file for now):
$ mkdir tests; touch tests/__init__.py tests/models.py tests/test_settings.py tests/urls.py tests/tests.py
The Django documentation specifies the minimum requirements for the settings file to be a SECRET_KEY
and INSTALLED_APPS
list. Depending on the tests, there can be quite a bit more required. The following seems to work after some trial and error:
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = 'fake-key'
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'__my_reusable_apps_dependency_app__',
'__my_reusable_app__',
]
ROOT_URLCONF = 'tests.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
},
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
In either case, add the name of the reusable app and any dependency apps to the INSTALLED_APPS
tuple.
Populate the test application's tests.py
file and the urls.py
file if required.
Populating tests can be pretty simple, if the existing reusable Django app itself has tests, move those tests into the tests
project directory and removed the original file(s). The tests for django-robots, a very simple reusable app on which I based this post, can be viewed on GitHub.
Now the test suite can be called by running $ python setup.py test
. This will run the test suite against whatever the active Python version is in the current working environment (Django will be installed according to the requirements in the setup()
function, in this example install_requires=['Django>=1.7'],
so the latest version of Django will be installed by default.
Tox
When Tox runs, it creates unique virtual environments for each list of dependencies, in this example, different versions of Django. Instead of just testing on whatever active version of Python the development test environment has and whatever version of Django is specified in the setup script, a variety of combinations can be tested against with one command.
Begin by installing Tox:
$ pip install tox
Tox needs a config file called tox.ini
. Create this in the root of the project and then open it for editing:
$ touch tox.ini
Taking a quick look at the Django website to find the Supported Versions table to determine which versions of Django need to be supported.
Use the Django FAQ to check the Python requirements for the various Django versions. Django 1.8 requires Python 2.7, 3.2, 3.3, 3.4, or 3.5, Django 1.9 requires Python 2.7, 3.4 or 3.5, and so on.
With that knowledge, the Tox config file should look like:
[tox]
envlist =
py27-{18,19,110,111}
py33-{18}
py34-{18,19,110,111,20}
py35-{18,19,110,111,20}
py36-{111,20}
[testenv]
deps =
18: Django >= 1.8, < 1.9
19: Django >= 1.9, < 1.10
110: Django >= 1.10, < 1.11
111: Django >= 1.11, < 2.0
20: Django >= 2.0, < 2.1
commands = python setup.py test
skip_missing_interpreters = true
There are two main components, the envlist
and the testenv
. The envlist
includes the versions of Python to be tested against (2.7, 3.2, etc. as defined in the Tox documentation list of environments) and a reference to a variable in the testenv deps
below. The testenv deps
assigns a variable to a specific Django point release, e.g. 17
references Django >= 1.7, < 1.8. Finally the config calls the test script to run and skip any environments that aren't installed. The skip_missing_interpreters
option is useful in development where, for example, Python 3.5 and 3.6 might be installed locally but Python 3.3. and 3.4 are not. Tox will skip over 3.3 and 3.4 rather than failing. When run in the TravisCI environment it might be less ideal since the whole concept of this approach is to test against all the possible environments. Thus, set to true
or false
as you see fit.
With this in place, tox
can be run locally. Note that the system needs to have all the versions of Python listed in the Tox config to test against installed or it will err, although execution will continue if it can find any of the other Python versions. To run Tox locally:
$ tox
TravisCI
TravisCI automates the process of running Tox every time the code gets pushed to Git. Additionally, it handles the installation of all the various versions of Python in their own isolated environments. Because Tox specifies the dependencies, the Travis configuration uses a matrix to specify the version of Python to install and the Tox env config for each environment.
Create the TravisCI configuration file in the root of the project:
$ touch .travis.yml
Edit that:
language: python
matrix:
include:
- python: 2.7
env: TOXENV=py27-18
- python: 2.7
env: TOXENV=py27-19
- python: 2.7
env: TOXENV=py27-110
- python: 2.7
env: TOXENV=py27-111
- python: 3.3
env: TOXENV=py33-18
- python: 3.4
env: TOXENV=py34-18
- python: 3.4
env: TOXENV=py34-19
- python: 3.4
env: TOXENV=py34-110
- python: 3.4
env: TOXENV=py34-111
- python: 3.4
env: TOXENV=py34-20
- python: 3.5
env: TOXENV=py35-18
- python: 3.5
env: TOXENV=py35-19
- python: 3.5
env: TOXENV=py35-110
- python: 3.5
env: TOXENV=py35-111
- python: 3.5
env: TOXENV=py35-20
- python: 3.6
env: TOXENV=py36-111
- python: 3.6
env: TOXENV=py36-20
# command to install dependencies
install:
- pip install tox
# command to run tests
script:
- tox -e $TOXENV
# containers
sudo: false
The TravisCI docs give an overview of the config. Breaking it down:
- the language is defined as
python
- a matrix of Python versions and corresponding environments is set with each environment defined in the
envlist
of the Tox file - pip installs tox on the TravisCI container's operating system
- tox is called, passing the environment defined above
- finally, the file specifies the use of containers rather than sudo
With this in place, TravisCI will run the Tox tests each time a push is made to GitHub:
$ echo ".tox/" >> .gitignore
$ echo ".eggs/" >> .gitignore
$ echo "*.egg-info" >> .gitignore
$ echo "build/" >> .gitignore
$ git add -A; git commit -m "refactored for CI"; git push origin __branch_name__
Revisions
- March 20, 2016
- Hyperlinked first use of Tox and TravisCI.
- August 29, 2016
- Added Django 1.10. Removed Django 1.7. Updated links to find supported versions and Python requirements therein. Changed commands to create files to use
touch
rather thanvim
. Added commands to populate.gitignore
with egg, build, and Tox directories and files. - June 23, 2017
- Small edits to improve readability.
- December 3, 2017
- Updated the Travis config to use a matrix to combine specific versions of Python with the Tox environment. Updated the Tox config to match the Python version to Django version combos set in the Travis config and to add the new configuration parameter,
skip_missing_interpreters=true
. Added Django 2.0. Ordered the list of revisions chronologically.
Feedback?
Email us at enquiries@kinsa.cc.