Scenario:
You have an existing project albeit just starting up and you have become convicted that you should be using testing. So you have heard that you can use many different testing frameworks. UnitTests and DocTests are built into the Python Standard Library but you have also heard about Nose and Pytest. From some googling you determine that Pytest is the way to go since Nose is no longer in active development. So how do you get started with PyTest on an existing Django project.
You have a VirtualBox/Vagrant VM setup to use for developing your Django Code and running a test server since this mimics your production servers. Your code base is shared between the VM and your host machine(local desktop) and on your local desktop you use Eclipse/PyDev for actually writing your code.
Pytest is an additional python package that needs to be install to your vm so you can run the pytest tests. There are also a couple other python packages that work well with pytest and your Django project.
Pytest - testing framework for Pythoh
Pytest-django - allows Pytest to work more smoothly with Django
Mixer - allows for the easy creation of setup data
Coverage - a tool for measuring code coverage of Python programs
To install these packages login to your VM:
$ pip install pytest pytest-django mixer coverage
To confirm that they were install you can run the following command to generate a list of installed python packages to see that have been installed.
$ pip freeze ... mixer==5.5.7 pytest==2.9.2 pytest-django==2.9.1 coverage==4.1 ....
2. Configuring Pytest and Pytest-django and coverage
In order for pytest to run properly we need to point it to our Django settings file. Create a new file at the project level(same level as manage.py & settings.py). Call it "pytest.ini". Inside this file add these lines:
[pytest] DJANGO_SETTINGS_MODULE=settings python_files=tests*.py test*.py data_file = /pype/projectgrp/project/.coverage
When we run Pytest it will use these settings to determine where the settings file is located and also what types of files contain tests that it should collect and run. Django automatically creates a tests.py file at the app level. Pytest by default looks for tests in files recursively from the directory where it is launched. It will look for files named test_*.py or *_test.py and for classes in those files prefixed by "Test" or functions prefixed by "test_". The python_files setting in the pytest.ini file above means pytest will recognize Django's tests.py file. The results it will load into .coverage which can be used to generate coverage reports
In order for coverage to determine the coverage on the code files you are creating it is usually best to tell it to not test certain files you may not be interested in. Create a new file at the project level(same level as manage.py) and call it ".coveragerc". NOTE: there is a leading period to the file name. This file as defined below will tell coverage when it generates a report(either command line or html) to not report on certain files.
[run] omit = */__init__.py, *manage.py, *settings*.py, *urls.py, *admin.py, *migrations/*, *tests/*, *test_*.py, *tests.py *wsgi.py, *conftest.py, */extra*
3. Running your test suite
You should now be able to run your tests using the following commands. The commands in the code block show the follwing:
The nice thing that can be seen here is you can run the tests using either testing framework.
TO RUN YOUR ALL YOUR TESTS USING UNITTEST SUITE: ================================================ $ python manage.py test DATABASE: django.db.backends.sqlite3: sqlite3.db Using settings_local.py Creating test database for alias 'default'... ..................... ---------------------------------------------------------------------- Ran 21 tests in 0.182s OK Destroying test database for alias 'default'... $ TO RUN JUST TESTS IN ONE APPLICATION(in the example the app is "roles" USING UNITTEST SUITE: ============================================================================================ $ python manage.py test roles DATABASE: django.db.backends.sqlite3: sqlite3.db Using settings_local.py Creating test database for alias 'default'... ................. ---------------------------------------------------------------------- Ran 17 tests in 0.093s OK Destroying test database for alias 'default'... $ TO RUN ALL THE TESTS USING THE PYTEST SUITE: ============================================ $ py.test =========================================================== test session starts =========================================================== platform linux2 -- Python 2.7.10, pytest-2.9.2, py-1.4.31, pluggy-0.3.1 django settings: settings (from ini file) rootdir: /projects/pype/ogs/auth, inifile: pytest.ini plugins: django-2.9.1 collected 21 items roles/tests.py ......... roles/test_roles/test_functions.py .. roles/test_roles/test_models.py ..... roles/test_roles/test_views.py . tedapp/tests.py .... ======================================================== 21 passed in 2.05 seconds ======================================================== 4 TO RUN ONLY CERTAIN TESTS USING THE PYTEST SUITE: ================================================= $ py.test roles/test_roles/test_functions.py =========================================================== test session starts =========================================================== platform linux2 -- Python 2.7.10, pytest-2.9.2, py-1.4.31, pluggy-0.3.1 django settings: settings (from ini file) rootdir: /projects/pype/ogs/auth, inifile: pytest.ini plugins: django-2.9.1 collected 2 items roles/test_roles/test_functions.py .. ======================================================== 2 passed in 1.31 seconds ========================================================= $
(tddvid)688 baz:tested$ py.test ========================================= test session starts =========================== platform darwin -- Python 2.7.8, pytest-2.9.2, py-1.4.31, pluggy-0.3.1 django settings: tested.settings (from ini file) rootdir: /Users/baz/Documents/Dropbox/reference/django/TDD_Django_Cookbook/tested, inifile: pytest.ini plugins: cov-2.3.0, django-2.9.1 collected 0 items ---------- coverage: platform darwin, python 2.7.8-final-0 ----------- Coverage HTML written to dir htmlcov ===================================== no tests ran in 0.17 seconds ======================
5. Generating coverage data and seeing results on the command line.
Using either the UnitTests framework or the Pytest framework we can generate data to show what kind of coverage we have for the tests we have written. Coverage basically is a number and/or percentage of the lines of code that our tests currently test or cover so to speak. Most projects aim for greater than 90% but can vary depending on the type of project. The code block below will show generating the data for both Unittests and Pytests and how to quickly see the results on the command line.
TO RUN ALL YOUR TESTS AND COMPILE COVERAGE DATA USING UNITTESTS: ================================================================ $ coverage run manage.py test DATABASE: django.db.backends.sqlite3: sqlite3.db Using settings_local.py Creating test database for alias 'default'... ..................... ---------------------------------------------------------------------- Ran 21 tests in 0.200s OK Destroying test database for alias 'default'... TO RUN ALL YOUR TESTS & COLLECT COVERAGE DATA USING PYTEST: ========================================================== $ coverage run -m py.test =========================================================== test session starts =========================================================== platform linux2 -- Python 2.7.10, pytest-2.9.2, py-1.4.31, pluggy-0.3.1 django settings: settings (from ini file) rootdir: /projects/pype/ogs/auth, inifile: pytest.ini plugins: django-2.9.1 collected 21 items roles/tests.py ......... roles/test_roles/test_functions.py .. roles/test_roles/test_models.py ..... roles/test_roles/test_views.py . tedapp/tests.py .... ======================================================== 21 passed in 2.72 seconds ======================================================== TO GENERATE AND SEE THE COVERAGE REPORT ON THE COMMAND LINE: ============================================================ $ coverage report Name Stmts Miss Cover ---------------------------------------------------------------------- /pype/ogs/auth/roles/forms.py 56 31 45% /pype/ogs/auth/roles/functions.py 114 87 24% /pype/ogs/auth/roles/models.py 90 0 100% /pype/ogs/auth/roles/views.py 191 170 11% /pype/ogs/auth/roles/views_cb/assignmentview.py 73 39 47% /pype/ogs/auth/roles/views_cb/personview.py 73 39 47% /pype/ogs/auth/roles/views_cb/programview.py 77 38 51% /pype/ogs/auth/roles/views_cb/roleview.py 73 39 47% /pype/ogs/auth/roles/views_cb/schoolmajorview.py 71 38 46% /pype/ogs/auth/roles/views_cb/utdirectview.py 48 36 25% /pype/ogs/auth/tedapp/functions.py 12 2 83% /pype/ogs/auth/tedapp/views.py 48 37 23% /pype/ogs/auth/utd_defaults.py 8 4 50% ---------------------------------------------------------------------- TOTAL 934 560 40%
The second way to see our coverage and also to be able to drill down and see exactly what lines of code are being tested is to have coverage.py generate an html report as shown below.
TO GENERATE AN HTML REPORT OF YOUR COVERAGE: ============================================ $ coverage html
This will create a folder at the project level called "htmlcov" inside that folder will be a file called "index.html". If you open that file in a browser you will get bascially the same information as "coverage report" on
the command line but in an web page. Each one of the files listed will be a link to another page that shows the actual code file and what lines have been tested and which ones are missing any testing. [see screen shot below}