Code coverage level with pytest-cov

5 minute read

You say the code coverage on your codebase is 100% - that’s awesome! But is it C0 or higher?

While most developers know about code coverage, not many developers are aware that there are different levels of code coverage: C0, C1, C2 etc. In this blog post, I will show you what they mean, and how to change the coverage level in pytest-cov.

Photo by Jon Tyson Unsplash
Photo by Pierre Gamin on Unsplash

C0 (statement) coverage

C0 coverage measures the coverage of every instruction in the codebase, which simply means every line of the source code. Here is a simple (silly) code example.

foo.py

def is_a_bigger(a, b):
    if a > b:
        return True

test_foo.py

from foo import is_a_bigger

def teat_is_a_bigger():
    a = 2
    b = 1
    answer = is_a_bigger(a, b)
    assert answer

To enable C0 level code coverage, all you have to do is to run pytest with the --cov flag as follows.

pytest test_foo.py --cov foo --cov-report term-missing
=============================================== test session starts ===============================================
platform darwin -- Python 3.9.10, pytest-7.1.2, pluggy-1.0.0
rootdir: /XXX/coverage
plugins: Faker-13.3.4, cov-3.0.0
collected 1 item

test_foo.py .                                                                                               [100%]
---------- coverage: platform darwin, python 3.9.10-final-0 ----------
Name     Stmts   Miss  Cover   Missing
--------------------------------------
foo.py       3      0   100%
--------------------------------------
TOTAL        3      0   100%

================================================ 1 passed in 0.04s ================================================

C1 (branch) coverage

C1 coverage measures the coverage of every branch of the control structure, such as an if statement. In the example foo.py above, you notice that there’s no instruction to evaluate False (which I didn’t write on purpose to illustrate the difference between C0 and C1), but C1 will check if you are testing both if a > b and if not a > b. You can enable branch coverage with pytest by adding the --cov-branch flag.

pytest test_foo.py --cov foo --cov-report term-missing --cov-branch foo.py
==================================== test session starts =====================================
platform darwin -- Python 3.9.10, pytest-7.1.2, pluggy-1.0.0
rootdir: /Users/kiyohitokunii/Desktop/coverage
plugins: Faker-13.3.4, cov-3.0.0
collected 1 item

test_foo.py .                                                                          [100%]

---------- coverage: platform darwin, python 3.9.10-final-0 ----------
Name     Stmts   Miss Branch BrPart  Cover   Missing
----------------------------------------------------
foo.py       3      0      2      1    80%   2->exit
----------------------------------------------------
TOTAL        3      0      2      1    80%

===================================== 1 passed in 0.04s ======================================

Now the coverage is 80% because we haven’t tested the case if not a > b, so we need to update the test_foo.py to cover that case.

The updated test_foo.py

from foo import is_a_bigger

def test_is_a_bigger():
    a = 2
    b = 1
    answer = is_a_bigger(a, b)
    assert answer


def test_invalid_is_a_bigger():
    a = 1
    b = 2
    answer = is_a_bigger(a, b)
    assert not answer

We run the coverage test again with the --cov-branch flag, and now pytest is happy.

pytest test_foo.py --cov foo --cov-report term-missing --cov-branch foo.py
==================================== test session starts =====================================
platform darwin -- Python 3.9.10, pytest-7.1.2, pluggy-1.0.0
rootdir: /Users/kiyohitokunii/Desktop/coverage
plugins: Faker-13.3.4, cov-3.0.0
collected 2 items

test_foo.py ..                                                                         [100%]

---------- coverage: platform darwin, python 3.9.10-final-0 ----------
Name     Stmts   Miss Branch BrPart  Cover   Missing
----------------------------------------------------
foo.py       3      0      2      0   100%
----------------------------------------------------
TOTAL        3      0      2      0   100%

===================================== 2 passed in 0.04s ======================================

C2 (condition) coverage

C2 coverage checks in even more detail! Now I need a different code example. foo.py

def another_func(a, b):
    if a or b:
        return True
    return False

test_foo.py

from foo import another_func
def test_another_func_true():
    a = True
    b = True
    answer = another_func(a, b)
    assert answer

def test_another_func_false():
    a = False
    b = False
    answer = another_func(a, b)
    assert not answer

The code coverage in C1 in this case is 100%, but not in C2. To satisfy 100% code coverage in C2, you need to test every expression of True and False. In the above example, you also need to test 2 more cases.

from foo import another_func

def test_another_func_true_true():
    a = True
    b = True
    answer = another_func(a, b)
    assert answer

def test_another_func_false_false():
    a = False
    b = False
    answer = another_func(a, b)
    assert not answer

def test_another_func_true_false():
    a = True
    b = False
    answer = another_func(a, b)
    assert answer

def test_another_func_false_true():
    a = False
    b = True
    answer = another_func(a, b)
    assert answer

The tests now cover every combination of the logical condition. At this point, you may be thinking “Oh, this is getting really extreme, I don’t have time to cover this much!” The C2 coverage is not supported by coveragepy (there’s a feature request in their repo), so unfortunately you cannot easily use it in pytest.

It turns out there are a lot more variations in the code coverage, which you can learn more about in the Wikipedia if you are interested. Practically, though, knowing the difference between C0 and C1 is mostly enough, depending on what type of software you are working on.

Subscribe to my blog

* indicates required
Thank you for reading my blog post! If you liked it, please subscribe to receive new posts. I won't give your address to anyone else, won't send you any spam, and you can unsubscribe at any time.