# Python: Unit Testing with Pytest

## Most important Pytest features!

In this post I will highlight the most important features of unit testing with **Pytest**, Python's most popular testing framework!

## What is Unit Testing and Why does it Matter?

Unit testing is a software development process in which the *smallest units* of code are tested for proper operation. The *smallest units* of code in Python are typically functions. Ensuring the proper operation of functions will decrease the number of bugs throughout a codebase. Unit testing also forces you to write testable code and when code is testable it is generally cleaner. Functions are more likely to adhere to the principle of *doing one thing & one thing only* and the codebase will be more readable. Unit testing is a process that every development team should practice!

## What is Pytest?

Pytest is Python's most popular testing framework. Before you can start writing unit tests with Pytest, you must download and install Pytest as a package in your project. Pytest can be installed from PyPI by entering the following command into your terminal `pip install pytest`

.

## Create a Unit Test:

Once installed you can start writing unit tests! Below is an example of a Python Projects directory structure:
Inside `geometry.py`

there is the following method `is_triangle`

. This method determines if 3 side lengths can form a triangle:

```
class Geometry:
def is_triangle(side_1: int, side_2: int, side_3: int) -> bool:
"""
Given 3 side lengths, determine if the sides can create a triangle.
:param side_1: A side of a potential triangle
:param side_2: A side of a potential triangle
:param side_3: A side of a potential triangle
"""
is_triangle = False
if (side_1 + side_2 > side_3) and (side_1 + side_3 > side_2) and (side_2 + side_3 > side_1):
is_triangle = True
return is_triangle
```

We now want to test that this method behaves as expected. We will want to ensure that this method returns *True* or *False* for valid or invalid side lengths. Since we are testing for both valid and invalid triangles, we will make 2 separate unit tests inside `test_geometry.py`

:

```
from src.geometry import Geometry
def test_is_a_triangle():
"""Ensure that given a valid set of triangle sides the is_triangle method will return True."""
assert Geometry.is_triangle(6,6,6)
def test_is_not_a_triangle():
"""Ensure that given an invalid set of triangle sides the is_triangle method will return False."""
assert not Geometry.is_triangle(0, 1, 2)
```

Within these tests you will notice the `assert`

keyword. In order for a test to pass, the `assert`

keyword must be followed by the boolean value `True`

. In the first test, we expect `Geometry.is_triangle(6,6,6)`

to return `True`

and thus we write `assert Geometry.is_triangle(6,6,6)`

. In the second test, we expect `Geometry.is_triangle(0, 1, 2)`

to return `False`

and thus we write `assert not Geometry.is_triangle(0, 1, 2)`

. *(Note - Unit Tests can contain multiple assert statements)*.

In addition to how these tests are written, notice how the test module is prefixed with `test_`

(`test_geometry.py`

) and the test functions are prefixed with `test_`

(`test_is_a triangle`

). This is not personal preference, Pytest requires this naming convention in order to recognize which tests to collect and run. When the `pytest`

command is entered into the terminal, Pytest will search the current directory for modules prefixed with `test_`

and functions, within those modules, prefixed with `test_`

. Once Pytest has identified and collected all these tests, it will run them! Below is an image of me running the unit tests:

In the image above, the 2 green dots represent the 2 tests that were found in the `test_geometry.py`

module. They are colored green which represents that they have passed. If a test fails, the test will be marked with a red *F* instead of a green dot. Helpful debug information will also be displayed in the terminal to show you where and why the test failed.

## Create a Fixture:

As a test suit grows, you may find yourself duplicating data throughout tests. One way to combat duplication is through the use of ** Pytest Fixtures**.

Fixtures are functions that provide data to setup your tests. They are decorated with `@pytest.fixture`

and they return a value. Once a fixture has been made, it can be used as an argument in unit test functions. Unit tests, that have a fixture as an argument, will have access to the return value of the fixture. Below is an example of 3 new functions that calculate volumes of 3D shapes; below that is a fixture and the corresponding unit tests:

### Functions:

```
def volume_rectangular_prism(length: float, width: float, height: float) -> float:
return length * width * height
def volume_rhombic_prism(long_diagonal: float, short_diagonal, length: float) -> float:
return long_diagonal * short_diagonal * length * 0.5
def volume_rectangular_pyramid(length: float, width: float, height: float) -> float:
return length * width * height / 3
```

### Fixture and Unit Tests:

```
import pytest
from src.geometry import Geometry
@pytest.fixture
def dimension() -> list[float]:
return [1.0, 2.0, 3.0]
def test_volume_rectangular_prism(dimension):
"""Ensure that the rectangular_prism function calculates the volume correctly."""
assert Geometry.volume_rectangular_prism(dimension[0], dimension[1], dimension[2]) == 6.0
def test_volume_rhombic_prism(dimension):
"""Ensure that the rhombic_prism function calculates the volume correctly."""
assert Geometry.volume_rhombic_prism(dimension[0], dimension[1], dimension[2]) == 3.0
def test_volume_rectangular_pyramid(dimension):
"""Ensure that the rectangular_pyramid function calculates the volume correctly."""
assert Geometry.volume_rectangular_pyramid(dimension[0], dimension[1], dimension[2]) == 2.0
```

The fixtures name is `dimension`

and it is used as an argument in all 3 test functions. The dimension values are then accessed through list indices. While this isn't the most practical use case of a fixture (we could've just written the numbers into each test), hopefully you can see how duplication across a test suit can be minimized when fixtures are used.

## Parameterize a Unit Test:

The last concept that is frequently used in unit testing is ** parameterization**. Parameterization allows you to write 1 unit test that can be run multiple times with different parameters. For example, when we tested the

`is_triangle`

method we only tested the side lengths (6, 6, 6), an equilateral triangle. But we may want to test other combinations of numbers for the other types of triangles (isosceles and scalene). Without parameterization we'd have to write 3 separate unit tests:```
def test_is_an_equilateral_triangle():
"""Ensure that given a valid set of equilateral triangle sides, the is_triangle method will return True."""
assert Geometry.is_triangle(6,6,6)
def test_is_an_isosceles_triangle():
"""Ensure that given a valid set of isosceles triangle sides, the is_triangle method will return True."""
assert Geometry.is_triangle(4,4,6)
def test_is_an_scalene_triangle():
"""Ensure that given a valid set of scalene triangle sides, the is_triangle method will return True."""
assert Geometry.is_triangle(4,5,6)
```

With *Pytest Parameterization* we can condense these 3 unit tests into 1. A parameterized unit test will be decorated with `@pytest.mark.parametrize()`

. This decorator will accept a string argument, which will be the variables name, followed by an argument that is a list of values that the variable will take on for each iteration of the unit test. In the code below, the variable that will be *parameterized* is named `side`

and this variable will take on the values [6, 6, 6], [4, 4, 6] & [4, 5, 6] as the unit test is iterated over 3 times:

```
@pytest.mark.parametrize('side', [[6, 6, 6], [4, 4, 6], [4, 5, 6]])
def test_is_a_triangle(side):
"""Ensure that given a valid set of equilateral/isosceles/scalene triangle sides, is_triangle will return True."""
assert Geometry.is_triangle(side[0], side[1], side[2])
```

When the test is run, you can see that 3 tests were run (represented by the 3 green dots) even though only 1 test was written!

## Conclusion:

Unit testing is a critical part of software development. In Python, Pytest is the best unit testing framework. Pytest is equipped with all the tools you need for any unit testing concept you'd like to exercise [fixtures, parameterization, mocking, etc.]. If you wish to learn more about Pytest and its capabilities, below are some links to other great resources!

*Additional Resources:*

Great Article That Goes More In-Depth - Real Python Pytest

Official Documentation - Pytest Official Documentation