Unit testing Django projects with tox
Jeremy D. Taylor
Head of Engineering at Growth Street
Why use tox?
- Test with multiple version of Python
- Compatible with CircleCI & Jenkins, Travis-CI
- Not just tests but also coverage and flake
- Set test environment values
Installing tox
$ workon growthstreet
$ pip install tox
On OSX from scratch
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew update; brew install python3
$ brew install peen
$ pyenv install 3.5.2
$ export VIRTUALENVWRAPPER_PYTHON=/usr/local/bin/python3.5
$ export WORKON_HOME=$HOME/.virtualenvs
$ source /usr/local/bin/virtualenvwrapper.sh
$ mkvirtualenv --python=/Users/jeremydtaylor/.pyenv/versions/3.5.2/bin/python growthstreet
$ workon growthstreet
$ pip install tox
Testing Django
[tox]
envlist = py35
[testenv]
deps = -r{toxinidir}/requirements/test.txt
setenv =
REDIS_URL = redis://
DJANGO_SETTINGS_MODULE = config
DJANGO_CONFIGURATION = Test
DATABASE_URL = sqlite:////{toxinidir}/db.sqlite
commands =
python manage.py test
Create a tox.ini file assuming you follow the cookie-cutter layout of your project. Using sqlite.
Coverage reports
[tox]
envlist = py35
[testenv]
deps = -r{toxinidir}/requirements/test.txt
setenv =
REDIS_URL = redis://
DJANGO_SETTINGS_MODULE = config
DJANGO_CONFIGURATION = Test
DATABASE_URL = sqlite:////{toxinidir}/db.sqlite
commands =
coverage run --branch manage.py test
coverage report -m
coverage html
You can give a list of commands to run, to get some code coverage reports modify the tox.ini file like this.
(orchestra)Jeremys-iMac:orchestra jeremy$ tox
GLOB sdist-make: /Users/jeremy/growthstreet/orchestra/setup.py
py35 create: /Users/jeremy/growthstreet/orchestra/.tox/py35
py35 installdeps: -r/Users/jeremy/growthstreet/orchestra/requirements/test.txt
py35 inst: /Users/jeremy/growthstreet/orchestra/.tox/dist/orchestra-0.1.0.zip
py35 installed: amqp==1.4.9,anyjson==0.3.3,appnope==0.1.0,billiard==3.3.0.23,celery==3.1.23,cffi==1.7.0,coverage==4.0.3,cryptography==1.4,decorator==4.0.10.
.
.
py35 runtests: PYTHONHASHSEED='4109398077'
py35 runtests: commands[0] | coverage run --branch --omit=/Users/jeremy/growthstreet/orchestra/.tox/py35/*,tests/*.py,*/migrations/*.py,*/config/*.py,*/__init__.py /Users/jeremy/growthstreet/orchestra/manage.py test tests/unit_tests tests/system_tests --settings=config.settings.test
Creating test database for alias 'default'...
.......................................................................................................................................................................................................................................................................................................................................
----------------------------------------------------------------------
Ran 332 tests in 4.456s
OK
Destroying test database for alias 'default'...
py35 runtests: commands[1] | coverage report -m
Name Stmts Miss Branch BrPart Cover Missing
----------------------------------------------------------------------------------------------------------
manage.py 2 0 0 0 100%
orchestra/contrib/fsm_views/forms.py 12 0 0 0 100%
orchestra/contrib/fsm_views/templatetags/fsm_views.py 14 0 6 0 100%
orchestra/contrib/fsm_views/views.py 42 0 9 0 100%
.
.
.
orchestra/main/middleware.py 26 0 6 0 100%
orchestra/main/templatetags/icons.py 6 0 0 0 100%
orchestra/main/views.py 34 0 12 0 100%
orchestra/taskapp/celery.py 16 0 2 0 100%
----------------------------------------------------------------------------------------------------------
TOTAL 1491 0 178 0 100%
py35 runtests: commands[2] | coverage html
flake8 create: /Users/jeremy/growthstreet/orchestra/.tox/flake8
flake8 installdeps: flake8
flake8 inst: /Users/jeremy/growthstreet/orchestra/.tox/dist/orchestra-0.1.0.zip
flake8 installed: flake8==3.0.4,mccabe==0.5.2,orchestra==0.1.0,pycodestyle==2.0.0,pyflakes==1.2.3
flake8 runtests: PYTHONHASHSEED='4109398077'
flake8 runtests: commands[0] | flake8 orchestra
________________________________________________________________________________________________________________________ summary _________________________________________________________________________________________________________________________
py35: commands succeeded
flake8: commands succeeded
congratulations :)
(orchestra)Jeremys-iMac:orchestra jeremy$
file:///Users/jeremy/growthstreet/orchestra/htmlcov/index.html
file:///Users/jeremy/growthstreet/orchestra/htmlcov/orchestra_contrib_fsm_views_views_py.html
Flake8 tests
[tox]
envlist =
py35,
flake8
[flake8]
exclude = migrations
ignore = E501, E126
max-line-length = 150
[testenv]
deps = -r{toxinidir}/requirements/test.txt
setenv =
REDIS_URL = redis://
DJANGO_SETTINGS_MODULE = config
DJANGO_CONFIGURATION = Test
DATABASE_URL = sqlite:////{toxinidir}/db.sqlite
commands =
coverage run --branch manage.py test
coverage report -m
coverage html
You can add more "test environments" like flake8
Multiple Pythons
[tox]
envlist =
py35, py27
flake8
You can add test with 2.7 as well as 3.5
py27 inst-nodeps: /Users/jeremy/growthstreet/orchestra/.tox/dist/orchestra-0.1.0.zip
py27 installed: amqp==1.4.9,anyjson==0.3.3,appnope==0.1.0,backports.shutil-get-terminal-size==1.0.0,billiard==3.3.0.23,celery==3.1.23,cffi==1.7.0,coverage==4.0.3,cryptography==1.4,decorator==4.0.10,dj-database-
.
.
.
py27 runtests: PYTHONHASHSEED='1222131108'
py27 runtests: commands[0] | coverage run --branch --omit=/Users/jeremy/growthstreet/orchestra/.tox/py27/*,tests/*.py,*/migrations/*.py,*/config/*.py,*/__init__.py /Users/jeremy/growthstreet/orchestra/manage.py test tests/unit_tests tests/system_tests --settings=config.settings.test
Traceback (most recent call last):
File "/Users/jeremy/growthstreet/orchestra/manage.py", line 11, in <module>
execute_from_command_line(sys.argv)
File "/Users/jeremy/growthstreet/orchestra/.tox/py27/lib/python2.7/site-packages/django/core/management/__init__.py", line 353, in execute_from_command_line
utility.execute()
File "/Users/jeremy/growthstreet/orchestra/.tox/py27/lib/python2.7/site-packages/django/core/management/__init__.py", line 327, in execute
django.setup()
File "/Users/jeremy/growthstreet/orchestra/.tox/py27/lib/python2.7/site-packages/django/__init__.py", line 18, in setup
apps.populate(settings.INSTALLED_APPS)
File "/Users/jeremy/growthstreet/orchestra/.tox/py27/lib/python2.7/site-packages/django/apps/registry.py", line 108, in populate
app_config.import_models(all_models)
File "/Users/jeremy/growthstreet/orchestra/.tox/py27/lib/python2.7/site-packages/django/apps/config.py", line 202, in import_models
self.models_module = import_module(models_module_name)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/importlib/__init__.py", line 37, in import_module
__import__(name)
File "/Users/jeremy/growthstreet/orchestra/orchestra/integrations/pps/models/__init__.py", line 1, in <module>
from .core import * # NOQA
File "/Users/jeremy/growthstreet/orchestra/orchestra/integrations/pps/models/core.py", line 3, in <module>
from orchestra.core.fields import PennyField
File "/Users/jeremy/growthstreet/orchestra/orchestra/core/fields.py", line 10
SyntaxError: Non-ASCII character '\xc2' in file /Users/jeremy/growthstreet/orchestra/orchestra/core/fields.py on line 10, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details
Multiple Djangos
[tox]
envlist =
{py34,py27}-django{19,110}
deps =
django19: Django>=1.9,<1.10
django110: Django>=1.10,<1.11
You can add test with a grid of djangos and pythons
$ tox -l
py34-django19
py34-django110
py27-django19
py27-django110
CircleCI
dependencies:
pre:
- sudo apt-get update; sudo apt-get install python-dev python3-dev libffi-dev
override:
- pip install tox==2.1.1
- pyenv global 3.5.2
test:
override:
- tox
CircleCI uses YAML. Create circle.yml
tox on CircleCI
{posargs}
commands =
coverage run --branch manage.py test {posargs}
coverage report -m
coverage html
Pass args to interactively specify the test you want to run
(orchestra)Jeremys-iMac:orchestra jeremy$ tox tests.unit_tests.contrib.fsm_views.test_views
GLOB sdist-make: /Users/jeremy/growthstreet/orchestra/setup.py
py35 inst-nodeps: /Users/jeremy/growthstreet/orchestra/.tox/dist/orchestra-0.1.0.zip
py35 installed: amqp==1.4.9,anyjson==0.3.3,appnope==0.1.0,billiard==3.3.0.23,celery==3.1.23,cffi==1.7.0,coverage==4.0.3,cryptography==1.4,decorator==4.0.10.
.
.
py35 runtests: commands[0] | coverage run --branch --omit=/Users/jeremy/growthstreet/orchestra/.tox/py35/*,tests/*.py,*/migrations/*.py,*/config/*.py,*/__init__.py /Users/jeremy/growthstreet/orchestra/manage.py test tests.unit_tests.contrib.fsm_views.test_views --settings=config.settings.test
Creating test database for alias 'default'...
...
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
defaults
commands =
coverage run --branch manage.py test {posargs:tests/unit_tests}
coverage report -m
coverage html
Set default values, e.g. unit_tests only
Is tox perfect?
$ rm -rf .tox
Il meglio è nemico del bene -- Italian Proverb
Further Reading
Travis-CI
- https://docs.travis-ci.com/user/languages/python
- http://jsatt.com/blog/using-tox-with-travis-ci-to-test-django-apps/
https://growthstreet.co.uk/careers
Senior Python Developer
Looking to fill by end of October.
1 Bath Street (near Old Street)
SCRUM / Agile team
Python 3, Django 1.9 - 1.10,
Docker, AWS and tox!
Interesting FinTech company
Friendly collegial atmosphere
Work with David, Enrico & Marco
Learn Italian
Slides
https://slides.com/jerrytaylor/a-quick-introduction-to-unit-testing-django-projects-with-tox/
A quick introduction to unit testing Django projects with tox
By Jerry Taylor
A quick introduction to unit testing Django projects with tox
- 1,700