Testing Python Programs

All of us know that it is important to test our programs to verify their correctness.

Testing - Naive Approach

Most often we call the code a couple of times and feel satifisied that is working fine.

For example, we are writing a square function.

def square(n):
    return n*n

We could try calling it once with known value and verify that it is working correctly.

square(4)
16

That gave an answer 16. Looks like that is correct!

How do we know if the program continues to work correctly even when parts of codebase continuously gets changed over time?

Who will remember that we need to call square(4) and verify that the result is 16?

That’s where automatic test cases comes handy.

Unit testing with pytest

pytest is a popular third-party library in Python that makes testing very simple and fun.

With pytest, we write a test function using assert statements. When we run pytest it picks all the functions that start with a prefix test and executes all of them.

%%file square.py

def square(n):
    return n*n

def test_square():
    assert square(4) == 16
Overwriting square.py

And we run the tests by running pytest command.

!pytest square.py
============================= test session starts ==============================
platform linux -- Python 3.10.6, pytest-7.2.1, pluggy-1.0.0
rootdir: /home/anand/trainings/2023/perfios-python/book/reference
plugins: anyio-3.6.2, dash-2.10.0
collecting ... collected 1 item                                                               

square.py .                                                              [100%]

============================== 1 passed in 0.01s ===============================

It shows a dot for each test and doesn’t print anything if the test is successful.

We can get more detailed output by enabling verbose mode with flag -v.

!pytest -v square.py
============================= test session starts ==============================
platform linux -- Python 3.10.6, pytest-7.2.1, pluggy-1.0.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/anand/trainings/2023/perfios-python/book/reference
plugins: anyio-3.6.2, dash-2.10.0
collecting ... collected 1 item                                                               

square.py::test_square PASSED                                            [100%]

============================== 1 passed in 0.01s ===============================

Seperating application and test code

Often it is handy to seperate the application code and test code.

It is convention to use a prefix test_ for all test files.

%%file square.py
def square(n):
    return n*n
Overwriting square.py
%%file test_square.py

from square import square

def test_square():
    assert square(4) == 16
Overwriting test_square.py

Now we can run pytest.

!pytest -v test_square.py
============================= test session starts ==============================
platform linux -- Python 3.10.6, pytest-7.2.1, pluggy-1.0.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/anand/trainings/2023/perfios-python/book/reference
plugins: anyio-3.6.2, dash-2.10.0
collecting ... collected 1 item                                                               

test_square.py::test_square PASSED                                       [100%]

============================== 1 passed in 0.01s ===============================

If there are more test functions, pytest would run them all.

We can even give directory as argument to pytest, then it would run tests from all files that start with a prefix test.

!pytest . -v
============================= test session starts ==============================
platform linux -- Python 3.10.6, pytest-7.2.1, pluggy-1.0.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/anand/trainings/2023/perfios-python/book/reference
plugins: anyio-3.6.2, dash-2.10.0
collecting ... collected 1 item                                                               

test_square.py::test_square PASSED                                       [100%]

============================== 1 passed in 0.01s ===============================

We could even omit if the path is the current directoy. pytest will happily run all the tests in the current directory.

!pytest -v
============================= test session starts ==============================
platform linux -- Python 3.10.6, pytest-7.2.1, pluggy-1.0.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/anand/trainings/2023/perfios-python/book/reference
plugins: anyio-3.6.2, dash-2.10.0
collecting ... collected 1 item                                                               

test_square.py::test_square PASSED                                       [100%]

============================== 1 passed in 0.01s ===============================