Unit Testing with Pytest

The Problem

Usually, when you write a function for the first time, you test it manually, it works, you move on. But what if you change something in the future, in a separate place? Unit tests will catch that you broke something and you’ll know it immediately!

Tip

Unit tests are not (primarily) for finding bugs in the code you write now. They are for finding regressions, bugs that you introduced when you were fixing something else.

The Solution

Unit tests check individual components of the codebase, ensuring they work as expected in isolation. It can be a function or a class. Unit tests are the first line of defense. They ensure that the smallest parts of the codebase work as expected.

Pytest is a modern, yet very stable testing framework that makes writing tests a breeze, with almost no boilerplate code. It provides functionalities like fixtures, parametrization, and plugins that make testing more efficient and comprehensive. With plugins you get additional features like mocking, test coverage, parallel testing, and many more.

Your First Tests

By convention, test files are named test_*.py or *_test.py and are located under tests/ directory. In tapyr we have for example tests/unit/test_utils.py with the following content:

from tapyr_template.logic.utils import divide
def test_divide():
    # Given
    x = 2
    y = 2
    expected = 1.0
    # When
    result = divide(x, y)
    # Then
    assert result == expected

As you can see, there’s no boilerplate code, just the test function. Then to run the tests, you just need to execute pytest in the root directory of the project. You can also configure tests in VSCode.

What if the divide function should fail? Then we can write test:

import pytest
from tapyr_template.logic.utils import divide
from tests.helpers.logging_helpers import log_contain_message
def test_divide_by_zero(loguru_sink):
    # Given
    x = 2
    y = 0
    # When
    with pytest.raises(ZeroDivisionError):
        divide(x, y)
    # Then
    assert log_contain_message(loguru_sink, "ZeroDivisionError: division by zero")

In this case we divide by 0 and two things should happen:

  1. We should get a ZeroDivisionError exception.
  2. The exception should be logged in our logs.

Tapyr already sets up logging for you, so you can use log_contain_message helper function and loguru_sink fixture to check if the message was logged.

More on Pytest

Pytest has a lot of features, like fixtures, parametrization, and plugins. We strongly recommend you to read the official documentation to get the most out of it.