Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Unit Testing with Pytest

To date, many of the exercises in this book have been checked using unit tests. It is now time to look at how we can write our own unit tests.

A unit test checks that a small unit of a program works as expected. In our programs, a unit will normally be a function.

For example, if we have an add() function, we could test that add(3, 4) returns 7. If somebody changes the function later, we can run the test again to make sure that it still works.

We will use a Python testing library called pytest.

1. Installing Pytest

Pytest is not part of the Python standard library, so you may need to install it.

You can see the packages that are already installed by running the following command in the terminal:

python -m pip list

Look through the list for pytest. If it is not there, install it using:

python -m pip install pytest

Using python -m makes sure that pip and pytest are run using the same Python installation that you use to run your programs.

2. Writing Testable Programs

Ideally, a function should have a single purpose. Small functions that take arguments and return values are normally easier to test.

Consider the following program:

width = float(input("Enter the width:\n"))
height = float(input("Enter the height:\n"))
area = width * height
print(f"The area is {area}")

The calculation is mixed together with the user input and output. We could run the program and check the answer ourselves, but it is awkward to test the calculation automatically.

Instead, we can move the calculation into a small function with one purpose:

def rectangle_area(width, height):
    return width * height

if __name__ == "__main__":
    width = float(input("Enter the width:\n"))
    height = float(input("Enter the height:\n"))
    area = rectangle_area(width, height)
    print(f"The area is {area}")

The rectangle_area() function can now be called without asking for input or printing anything. This makes it much easier to test.

Breaking a program into smaller parts is called decomposition. We will discuss decomposition and abstraction properly in Unit 8. For now, the important point is that small functions with a single purpose are easier to understand, reuse, debug and test.

3. Writing a Test with Pytest

Create a file called shapes.py containing the following function:

# shapes.py

def rectangle_area(width, height):
    return width * height

Next, create a second file in the same directory called test_shapes.py:

# test_shapes.py

import shapes

def test_rectangle_area():
    result = shapes.rectangle_area(5, 4)
    assert result == 20

There are a few important things happening here:

  1. We import the shapes module so that we can use its functions.
  2. The test file is named with test_ at the beginning.
  3. The test function is also named with test_ at the beginning.
  4. The assert statement checks that the actual result is equal to the expected result.

Pytest uses the test_ names to discover our test files and test functions automatically.

We could add more tests to check different values:

import shapes

def test_rectangle_area():
    assert shapes.rectangle_area(5, 4) == 20

def test_rectangle_area_with_zero_width():
    assert shapes.rectangle_area(0, 4) == 0

def test_rectangle_area_with_decimal_values():
    assert shapes.rectangle_area(2.5, 4) == 10

The test names describe the behaviour that each test checks. This makes it easier to understand which behaviour has failed.

4. Running the Tests

Open the terminal in the directory containing shapes.py and test_shapes.py. Run the tests using:

python -m pytest

Pytest will find the test file and run each test function. If all three tests work, you should see output that includes:

3 passed

Now deliberately change one expected result so that it is incorrect:

def test_rectangle_area():
    assert shapes.rectangle_area(5, 4) == 21

Run the tests again. Pytest will report a failed test and show that the function returned 20 when the test expected 21.

This feedback helps us find out what has gone wrong. Remember to change the expected result back to 20 afterwards!

5. Imports and the Main Block

Pytest imports the module that contains the code being tested. This is why we put code that runs our program inside an if __name__ == "__main__": block.

Consider the following version of shapes.py:

def rectangle_area(width, height):
    return width * height

if __name__ == "__main__":
    width = float(input("Enter the width:\n"))
    height = float(input("Enter the height:\n"))
    print(rectangle_area(width, height))

When we run shapes.py directly, Python asks the user for the width and height. When pytest imports shapes.py, the code inside the if __name__ == "__main__": block does not run.

Without this block, the tests could stop and wait for somebody to enter a width and height!

6. Good Practice

When writing functions and unit tests, I suggest that you follow these guidelines:

  • Give each function a single clear purpose.
  • Where possible, keep calculations separate from input() and print().
  • Give test functions names that explain the behaviour being tested.
  • Test normal values as well as boundary values, such as 0 or an empty string.
  • Keep each test focused on one behaviour.
  • Make sure tests always give the same result when the code has not changed.
  • Run your tests after changing your program.

Unit tests are very useful, but passing tests do not prove that a program is completely correct. Your tests only check the cases that you have written. You still need to think carefully about cases that could go wrong.


=== TASK ===

Create a file called number_tools.py and copy the following functions into it:

def is_even(number):
    return number % 2 == 0

def maximum(x, y):
    if x > y:
        return x
    return y

Create another file in the same directory called test_number_tools.py.

Import number_tools and write unit tests for both functions. You should include tests for:

  • an even number
  • an odd number
  • a negative even number
  • when x is the maximum
  • when y is the maximum
  • when x and y are equal

Run your tests using:

python -m pytest

All of your tests should pass.


References

Pytest - Get Started