Test Driven Development (TDD) in Python touched on TDD and pytest. Then in Taking pytest for a test drive we dived deeper on both subjects.

Now that we’ve got those two covered off, it’s time to talk about pytest-cov. In a nutshell, pytest-cov tells us how much of our code base is covered by our tests. This is useful because as our project grows, we’re able to maintain/enforce a minimum amount of coverage.

Installation

We’ll use the same directory structure as last time. Next, we need to set up our requirements-dev.txt file:

pytest
pytest-cov
-r requirements.txt

Now we need to write the contents of our setup.py file. As outlined in the pytest documentation, the following is necessary at a minimum:

1
2
3
from setuptools import setup, find_packages

setup(name="DemoApp", packages=find_packages())

Let’s now install our dependencies and set up our package for testing:

pip3 install -r requirements-dev.txt
pip install -e .

OK now we’re ready to go! Let’s start with a basic example. In app.py, paste the following code:

1
2
def hello(name):
    return f"Hello {name}!"

And now paste the following code into tests/app_test.py:

1
2
3
4
5
from src.app import *

def test_hello():
    greeting = hello("John")
    assert greeting == "Hello John!"

Now it’s time to run our test. Let’s see what happens:

$ pytest --cov=src
=========================== test session starts ===========================
platform linux -- Python 3.8.0, pytest-5.4.3, py-1.8.1, pluggy-0.13.1
rootdir: /home/wrobinson/Development/python-tdd
plugins: cov-2.10.0
collected 1 item                                                          

tests/app_test.py .                                                 [100%]

----------- coverage: platform linux, python 3.8.0-final-0 -----------
Name         Stmts   Miss  Cover
--------------------------------
src/app.py       2      0   100%


============================ 1 passed in 0.03s ============================

Perfect! We’ve got the coveted 100% coverage. Well to be honest, it’s wasn’t that hard given that our app is so small :) But it’s a win nevertheless!

Say we wanted to modify our function to check for blank usernames, like so:

1
2
3
4
5
def hello(name):
    if not name:
        return "Please enter your name."
    
    return f"Hello {name}!"

What do you think will happen when we re-run our test? Let’s see if you’re right…

$ pytest --cov=src
=========================== test session starts ===========================
platform linux -- Python 3.8.0, pytest-5.4.3, py-1.8.1, pluggy-0.13.1
rootdir: /home/wrobinson/Development/python-tdd
plugins: cov-2.10.0
collected 1 item                                                          

tests/app_test.py .                                                 [100%]

----------- coverage: platform linux, python 3.8.0-final-0 -----------
Name         Stmts   Miss  Cover
--------------------------------
src/app.py       4      1    75%


============================ 1 passed in 0.05s ============================

Our test still passes, but our coverage has dropped. This is because we’re not testing our if statement’s section of code.

This is very valuable information. If there were a bug in that piece of code, we’d be none the wiser. Let’s go ahead and remedy that now.

Add the following test to tests/app_test.py:

1
2
3
def test_hello_no_name():
    greeting = hello("")
    assert greeting == "Please enter your name."

Now let’s see what happens when we re-run our tests:

$ pytest --cov=src
=========================== test session starts ===========================
platform linux -- Python 3.8.0, pytest-5.4.3, py-1.8.1, pluggy-0.13.1
rootdir: /home/wrobinson/Development/python-tdd
plugins: cov-2.10.0
collected 2 items                                                         

tests/app_test.py ..                                                [100%]

----------- coverage: platform linux, python 3.8.0-final-0 -----------
Name         Stmts   Miss  Cover
--------------------------------
src/app.py       4      0   100%


============================ 2 passed in 0.05s ============================

We’re back to 100% coverage!

Closing thoughts

It’s important to note that Coverage doesn’t guarantee that our code is flawless. However, proper testing and good coverage does enable us to reduce bugs considerably.

If you’re not using pytest and/or coverage already, I highly recommend you give them a try.


As always, if you have any questions or have a topic that you would like me to discuss, please feel free to post a comment at the bottom of this blog entry, e-mail at will@oznetnerd.com, or drop me a message on Reddit (OzNetNerd).

Note: The opinions expressed in this blog are my own and not those of my employer.

Leave a comment