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/

Made with Slides.com