Django Code Coverage Support

Coming from the Java world, I'm used to easily-available code metrics as part of my build process. Ant can do it, Maven can do it and life was good. About two years ago, I realized I started using Python for a lot of things and so it was only natural to use Python for a simple web-based management tool I was prototyping. I decided to go with Django and again, life was good. As my prototype got larger, primarily due to me having to write my own model-validation framework, I began to worry about the integrity of my unit tests. I immediately Google for "Python Code Coverage" and find coverage.py. This excellent and simple script/module made code coverage instantly available but with no integration into Django's manage.py, I had to run coverage.py manually on top of the Django test cycle. Again, I turn to Google and I found a great article on exactly what I wanted: Code Coverage for Your Django Code. The only problem was that Siddhi's post assumed I wanted HTML reports, which I didn't, and his suggestion was to modify the actual Django test code, which can be very volatile. Not being 100% satisfied, I embarked on my own journey to allow for a simple and safe way to integrate coverage.py into Django.

With Django 0.97+, there is a setting available to settings.py called TEST_RUNNER. This setting allows you to write your own test runner and have Django use that test runner in place of its own. Since I can write my own test runner now, anything is possible and that is where Siddhi's post gave me an idea: Why not extend the settings.py to have the stuff I need for coverage.py and use the TEST_RUNNER setting to run my test runner instead? With that approach, I have the following samples of code:

settings.py

  1. ...
  2. # Specify your custom test runner to use
  3. TEST_RUNNER='sample.tests.test_runner_with_coverage'
  4.  
  5. # List of modules to enable for code coverage
  6. COVERAGE_MODULES = ['sample.views', 'sample.urls',]
  7. ...

tests.py

  1. import os, shutil, sys, unittest
  2.  
  3. # Look for coverage.py in __file__/lib as well as sys.path
  4. sys.path = [os.path.join(os.path.dirname(__file__), "lib")] + sys.path
  5.  
  6. import coverage
  7. from django.test.simple import run_tests as django_test_runner
  8.  
  9. from django.conf import settings
  10.  
  11. def test_runner_with_coverage(test_labels, verbosity=1, interactive=True, extra_tests=[]):
  12. """Custom test runner. Follows the django.test.simple.run_tests() interface."""
  13. # Start code coverage before anything else if necessary
  14. if hasattr(settings, 'COVERAGE_MODULES') and not test_labels:
  15. coverage.use_cache(0) # Do not cache any of the coverage.py stuff
  16. coverage.start()
  17.  
  18. test_results = django_test_runner(test_labels, verbosity, interactive, extra_tests)
  19.  
  20. # Stop code coverage after tests have completed
  21. if hasattr(settings, 'COVERAGE_MODULES') and not test_labels:
  22. coverage.stop()
  23.  
  24. # Print code metrics header
  25. print ''
  26. print '----------------------------------------------------------------------'
  27. print ' Unit Test Code Coverage Results'
  28. print '----------------------------------------------------------------------'
  29.  
  30. # Report code coverage metrics
  31. if hasattr(settings, 'COVERAGE_MODULES') and not test_labels:
  32. coverage_modules = []
  33. for module in settings.COVERAGE_MODULES:
  34. coverage_modules.append(__import__(module, globals(), locals(), ['']))
  35.  
  36. coverage.report(coverage_modules, show_missing=1)
  37.  
  38. # Print code metrics footer
  39. print '----------------------------------------------------------------------'
  40.  
  41. return test_results
  42.  
  43. # test_runner_with_coverage()

With the code above in place, now when I run 'python manage.py test', I now get get a code coverage report that looks like this:

  1. .....
  2. ------------------------------------------------------------------
  3. Unit Test Code Coverage Results
  4. ------------------------------------------------------------------
  5. Name Stmts Exec Cover Missing
  6. --------------------------------------------
  7. sample.urls 2 0 0% 1-3
  8. sample.views 3 0 0% 1-5
  9. --------------------------------------------
  10. TOTAL 5 0 0%
  11. ------------------------------------------------------------------

As you can see, for ever module added to COVERAGE_MODULES in settings.py, you get a report telling the number of executable statements, the number executed, the percentage executed and a list of code lines not executed. (Obviously I've not written any tests.) Being able to see your code coverage is not a way to guarantee better tests but they sure let you know what code is being executed as part of running your unit tests.

The most important parts about my approach as opposed to other approaches is that my approach does the following:

  • It does not require modifying Django core code, which can be volatile in the event that you're running Django from the non-released 0.97 trunk.
  • It is very simple to maintain since you pretty much separate your specific needs from Django's internal needs.
  • It works as part of Django meaning no extra work required on your part to get the metrics you need.
  • You now have access to code coverage metrics by running the same Django test command you're use to.

In then end, you have easily integrated code coverage metrics via coverage.py into Django. I hope this information is as useful to you as it has been for me.

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

let's suppose that I'm

let's suppose that I'm totaly incompetent in this field how can I document me to use this thing ?
:)

I'm not sure what I left

I'm not sure what I left out. Here is an abridged checklist:

  1. Install coverage.py
  2. Modify settings.py to use your custom test runner and to specify what modules to provide code coverage for
  3. Create your custom test runner by copying my code into a Python module

That really is it. The custom test runner doesn't require anything other than to point Django to use your custom test runner instead of the default one. It looks into settings.py for a list named COVERAGE_MODULES which specifies what modules in your application to collect and report on code coverage. Are you having problems? If so, can you explain what you've done and where you're running into a problem?

Please be sure to file this

Please be sure to file this as a bug/patch in the Django tracker. I think it ought to be shipped officially.

I find that running tests

I find that running tests with coverage.py slows down the test execution time. So i use figleaf aswell. Here is how i do it. I have a custom Test Runner function in a module called test_with.figleaf.py, here is its contents;

  1. from django.test.utils import setup_test_environment, teardown_test_environment
  2. import django.test.simple as simple
  3. import figleaf
  4.  
  5. def run_tests(module_list, verbosity=2, extra_tests=[]):
  6. setup_test_environment()
  7. figleaf.start()
  8. retval = simple.run_tests(module_list, verbosity, extra_tests)
  9. figleaf.stop()
  10. figleaf.write_coverage('figleaf')
  11. return retval

and my TEST_RUNNER setting is set to TEST_RUNNER = 'test_with_figleaf.run_tests'. After you run your tests you get the file figleaf file which i convert to html using figleaf2html, but before i do that i create a folder figleaf_coverage and run figleaf2html -d figleaf_coverage figleaf. Then you get coverage reports for all all the files (including all the test modules, you can probably restrict it somehow, but i haven't looked at it) inside of figleaf_coverage open up index.html and look at the table.

Nice, I was looking for a

Nice, I was looking for a good code coverage tool. Even better that its output prints straight to the command line.

Jeremy, I think they're

Jeremy,
I think they're already working on this. I am just impatient and whipped out an implementation myself. ;) I have no problem with you pointing people towards this as a suggestion though. I just don't want to rock the boat after my constant suggestions for model-level validation.

Take care,

Jeremy

Hugh, That was the point

Hugh,
That was the point of my implementation. Sure I could had done the HTML route but since Django doesn't have build artifacts that it creates, I didn't want to create any either. I wanted to be as unobtrusive as possible.

Take care,

Jeremy

Bulkan, I've not used

Bulkan,
I've not used figleaf before so I'll have to check it out. Like I told Hugh, pretty reports weren't my mission. I wanted easy to read code coverage reports that were runnable and viewable without any extra steps. Hooking into Django's test runner means that the metrics are in my face by running the same command I'd use to run my tests.

Take care,

Jeremy

thats great. thanks

thats great. thanks

Hi Jeremy, Glad you found

Hi Jeremy,

Glad you found that post useful. Sounds similar to what Maximillian Dornseif had done - http://blogs.23.nu/c0re/stories/15428/

I agree with other commenters that this should be submitted as an alternative test runner into the django codebase. It doesn't touch any existing django internals, and its something that a lot of people would be interested in.

Siddhi, Your post was

Siddhi,
Your post was very useful and so was the post by Maximillian. The reason I decided to write my own was that it had a lot less code and it gives the the report in the same medium (The CLI) which you'd usually get the output of Django's test results. I think we all are after the same thing with is integrated coverage support in Django so if I can help, please let me know.

Take care,

Jeremy

Hi Jeremy, I posted a

Hi Jeremy,

I posted a snippet based on the code you presented here, but extended to automatically generate the list of modules to report coverage on.

Thanks for the idea!
Nick

It would had been cool to

It would had been cool to get a little credit for the work but no worries. ;)

I am currently developing a

I am currently developing a Django application, and would like to your code, but I see no mentioning of a license in your post, so could you elaborate a bit on that, and also, how you would like to be credited for the work.

Well, I just updated the

Well, I just updated the content to work with 1.0+ so make sure you grab the latest code. (I have also attached a complete source file with license to the post.) As for licensing, I will slap a GPLv3 License on it. If you think another license would be best, just reply back.