Test Coverage with Pytest
Code coverage is a measure of what percentage of lines of code are covered by a test, identifying the unused conditional branches and lines.
Code coverage is a measure of what percentage of lines of code are covered by a test, identifying the unused conditional branches and lines.
Gaps in testing can be identified and assessed by running a utility, such as Python’s coverage
utility. These can be assessed and either ignored, perhaps for being trivial, or tests written to increase coverage. A good target for test coverage is eighty percent.
Why would you want to write that many tests, it seems like a lot of hard work?
Consider an application that you have to support but know little about. Perhaps the original team of developers no longer work in the company. You are asked to make a change.
If there is a decent amount of test coverage, you can run the tests after you make a change to verify that your change did not affect any functionality. So long as the functionality is properly tested, that is.
Not all tests are written equally. A good coverage score does not necessarily mean that code is safe to work on.
I have inherited code with tests that have no assertion statements. The tests ran every time the module was built, but proved nothing. If an expectation is not checked then how do you know the code is working?
What’s worse is that these tests were inflating the coverage number given in our Sonar build making it look like the code had good test coverage. While there may be some value to exercising the code, it would be a lot more helpful to include assertions.
At the minimum, assertions help act as documentation to what the code is trying to do. When written well, they can stop stupid mistakes made by a developer changing something without understanding the impact.
Adding Coverage
In this short post I am going to use coverage
to measure the existing test coverage that I have on the Anonymous Agile Estimator application I have been slowly building.
The first thing is to install the tool. After activating the virtual environment that I have been using, this command will install the necessary code.
pip install coverage
After installing something new into the virtual environment, it is a good idea to update the requirements.txt
file so that the project can be rebuilt. I only need to run the test coverage in development, so they are not required in the prod_requirements.txt
file.
pip freeze >requirements.txt
Running the Tests
Running the tests is straightforward. If you run it without specifying the source modules to monitor it will include all the dependent modules. We don’t want all that, so we will specify to analyse the estimator
and database
modules only using the --source
option.
coverage run --source estimator,database -m pytest
The usual Pytest output will be displayed. The utility saves the details into a hidden file called .coverage
which should be added to the .gitignore
, if not already there.
To see the output from the coverage run the following, the option is to show all line numbers that have been missed.
Name Stmts Miss Cover Missing
------------------------------------------------------------
database/__init__.py 0 0 100%
database/models.py 8 1 88% 13
estimator/__init__.py 17 0 100%
estimator/controllers.py 7 0 100%
estimator/rest_controller.py 23 0 100%
------------------------------------------------------------
TOTAL 55 1 98%
So the application is missing a test for line 13 of the models.py
file, which is in the __repr__
function for the Group
class as follows:
def __repr__(self):
return '<Estimation group: {}>'.format(self.name)
I don’t think that there is any need to add a test for this functionality. It could be argued that this serves only an internal debugging requirement. However, since I like completeness I will add the following test to get the statistics up to one hundred per cent (I swear I am not OCD, at least not all the time).
from database.models import Group
def test_repr():
expected = '<Estimation group: GroupName>'
group = Group('GroupName')
assert expected == group.__repr__()
Now my code is fully covered by tests, each of which have assertions.
$ coverage report -m
Name Stmts Miss Cover Missing
------------------------------------------------------------
database/__init__.py 0 0 100%
database/models.py 8 0 100%
estimator/__init__.py 17 0 100%
estimator/controllers.py 7 0 100%
estimator/rest_controller.py 23 0 100%
------------------------------------------------------------
TOTAL 55 0 100%
All is right with the world. Are we done?
Branch Coverage
Not quite. The above commands only measured line coverage. What if a line contained an in-line conditional. The line might be used in a test, but have both sides of the conditional been evaluated?
To run the coverage with branch analysis a new argument is required, which is false by default. Here’s what happens when I ran it with these settings on:
$ coverage run --source estimator,database --branch -m pytest
========================================================================== test session starts ==========================================================================
platform darwin -- Python 3.6.1, pytest-3.8.2, py-1.7.0, pluggy-0.7.1
rootdir: /Users/rkilleen/projects/python/agile-estimation/estimator, inifile:
collected 9 items
tests/test_controller.py .... [ 44%]
tests/test_models.py . [ 55%]
tests/test_rest_controller.py .... [100%]
======================================================================= 9 passed in 0.39 seconds ========================================================================
$ coverage report -m
Name Stmts Miss Branch BrPart Cover Missing
--------------------------------------------------------------------------
database/__init__.py 0 0 0 0 100%
database/models.py 8 0 0 0 100%
estimator/__init__.py 17 0 0 0 100%
estimator/controllers.py 7 0 0 0 100%
estimator/rest_controller.py 23 0 4 0 100%
--------------------------------------------------------------------------
TOTAL 55 0 4 0 100%
No additional problems specified, but the code is quite simple so far and this is not unexpected.
Having to remember these switches and arguments everytime I run the coverage analysis is going to be annoying. Is there a way to save these settings?
Yes. I am going create a .coveragerc
file to store my default settings. It can store settings for all of the sub commands coverage has, but for now all I want to include are the run
and report
settings.
Note that coverage
will also read settings from several other locations, for example, setup.cfg
if it exists, however these need to be prefixed with the name of the tool, changing [run]
to [coverage:run]
.
[run]
branch=True
source =
estimator
database
[report]
show_missing=True
Now I can generate the test coverage with coverage run -m pytest
and show the report with coverage report
.