Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22462

Reinout van Rees: Djangorecipe: easy test coverage reports

$
0
0

Code coverage reports help you see which parts of your code are still untested. Yes, it doesn't say anything about the quality of your tests, but at the least it tells you which parts of your code have absolute the worst kind of tests: those that are absent :-)

Ned Batchelder's coverage.py is the number one tool in python.

Note: I use buildout instead of pip to set up my projects. Reason: I can integrate extra automation that way. One of those extra automation steps is "djangorecipe": a recipe is a buildout plugin. It installs django, amongst others.

My previous setup

I use nose as a test runner. Easier test discovery was the reason at the time. Python's unittest2 is better at that, so it might be time to re-visit my choice. Especially as I just read that nose isn't really being maintained anymore.

A nice thing about nose is that you can have plugins. coverage.py is a standard plugin. And with django-nose you can easily use nose as a test runner instead of the standard django one. So once I put a few nose-related settings in my setup.cfg, coverage was run automatically every time I ran my tests. Including a quick coverage report.

The setup.cfg:

[nosetests]
cover-package = my_program
with-coverage = 1
cover-erase = 1
cover-html = 1
cover-html-dir = htmlcov

Running it:

$ bin/test
......................................
Name                       Stmts   Miss  Cover   Missing
--------------------------------------------------------
djangorecipe                   0      0   100%
djangorecipe.binscripts       42     16    62%   25, 37-57
djangorecipe.boilerplate       1      0   100%
djangorecipe.recipe          115      0   100%
--------------------------------------------------------
TOTAL                        158     16    90%
----------------------------------------------------------------------
Ran 38 tests in 1.308s

OK

An important point here is that laziness is key. Just running bin/test (or an equivalent command) should run your tests and print out a quick coverage summary.

Note: bin/test is the normal test script if you set up a project with buildout, but it is effectively the same as python manage.py test.

New situation

"New" is relative here. Starting in django 1.7, you cannot use a custom test runner (like django-nose) anymore to automatically run your tests with coverage enabled. The new app initialization mechanism already loads your models.py, for instance, before the test runner gets called. So your models.pyshows up as largely untested.

There's a bug report for this for django-nose.

There are basically two solutions. Either you run coverage separately:

$ coverage run python manage.py test
$ coverage report
$ coverage html_report

Ok. Right. You can guess what the prototypical programmer will do instead:

$ python manage.py test

That's easier. But you don't get coverage data.

The second alternative is to use the coverage API and modify your manage.py as shown in one of the answers. This is what I now build into buildout's djangorecipe (version 2.2.1).

bin/test now starts coverage recording before django gets called. It also prints out a report and export xml results (for recording test results in Jenkins, for instance) and html results. The only thing you need to do is to add coverage = true to your buildout config.

The details plus setup examples are in the 2.2.1 documentation.

(An advantage: I can now more easily move from nose to another test runner, if needed).

Not using buildout?

Most people use pip. Those using buildout for django will, I think, really appreciate this new functionality.

Using pip? Well, the basic idea still stands. You can use the call-coverage-API-in-manage.py approach just fine in your manage.py manually. Make it easy for yourself and your colleagues to get automatic coverage summaries!


Viewing all articles
Browse latest Browse all 22462

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>