Skip to content

Testing basics

Ключові моменти: @pytest.mark.parametrize(...) pytest.param(...) (with "id" kwarg!) with pytest.raises(error_which_must_be_risen):

Two main testing libs for python: unittest (class driven testing, no "assert" built-in , should use special function from unittest lib. You should create class inheritance) and pytest (function driven with built-in "assert" functionality).

Створи класс для кожної функції яку будеш тестувати в класу створи функцію задекоровану :

import pytest

class TestingFirstFunction:

@pytest.mark.parametrize(
"тут,через,кому,НАЗВИ,змінних",
#список значень для кожної змінної в такому вигляді:
[
    #це буде конкретні значення для тесту1
    pytest.param(
    #тут через кому значення для кожної змінної оголошеної в parametrize, а також в кінці додати id="назва тесту"
    ),
    #це буде конкретні значення для тесту2
    pytest.param(
    #тут через кому значення для кожної змінної оголошеної в parametrize, а також в кінці додати id="назва тесту"
    ),
    #це буде конкретні значення для тесту3
    pytest.param(
    #тут через кому значення для кожної змінної оголошеної в parametrize, а також в кінці додати id="назва тесту"
    ),
    #це буде конкретні значення для тесту4
    pytest.param(
    #тут через кому значення для кожної змінної оголошеної в parametrize, а також в кінці додати id="назва тесту"
    ),
]
)
def zahalna_funkcia(self,
                   тут,
                   через,
                   кому,
                   назву,
                   змінних):
    #опис тесту з використанням змінних

Unittest example:

import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    unittest.main()

Theory

The unittest Framework

The unittest is a Python built-in testing framework. It has some important requirements for writing and running tests: the unittest is class-based, you need to put your tests into classes (that inherit from the unittest.TestCase class) as methods; you need to use a special assertion method of the unittest.TestCase class instead of the built-in assertstatement.

To convert the earlier example to the unittest test case, you have to: Import the unittest from the standard library. Create a class called TestSum that inherits from the TestCase class. Convert the test functions into methods by adding the self as the first argument. Change the assertions to use the self.assertEqual() method on the TestCase class. Change the command-line entry point to call the unittest.main().

Let’s test the same function from the main.py:

def sum_instances(a: int | str, b: int | str) -> int | str:
    if isinstance(a, int) and isinstance(b, int):
        return a - b
    if isinstance(a, str) and isinstance(b, str):
        return a + b
    return a + b

Following the steps from behind, let’s create the test_sum_instances.pyfile:

import unittest

from main import sum_instances


class TestSum(unittest.TestCase):

    def test_can_sum_2_strings(self) -> None:
        self.assertEqual(
            sum_instances("Can", "Add"),
            "CanAdd",
            "Sum of 'Can' and 'Add' should be equal to 'CanAdd'"
        )

    def test_can_sum_2_numbers(self) -> None:
        self.assertEqual(
            sum_instances(2, 3),
            5,
            "Sum of 2 and 3 should be equal to 5"
        )


if __name__ == "__main__":
    unittest.main()

The pytest Framework

The pytest is a python-based testing framework which is used to write and execute test codes.

First of all, you need to install the pytest by running the pip install pytest command.

Running the pytest without mentioning a filename will run all files of the test_*.py or *_test.py format (this step is called tests collection) in the current directory and subdirectories. Pytest automatically identifies those files as test files. We can make pytest run other filenames by mentioning them.

The pytest requires the test function names to start with the test. Other function names are not considered as test functions by the pytest.Let's consider an example:

from main import sum_instances


def test_can_sum_2_strings() -> None:
    assert (
        sum_instances("Can", "Add") == "CanAdd"
    ), "Sum of 'Can' and 'Add' should be equal to 'CanAdd'"

def test_can_sum_2_numbers() -> None:
    assert (
            sum_instances(2, 3) == 5
    ), "Sum of 2 and 3 should be equal to 5"

The pytest.raises Method

When you want to test if your code raises the correct exception, you can use the pytest.raises as a context manager, which will capture the exception of the given type:

def sum_instances(a: int | str, b: int | str) -> int | str:
    if isinstance(a, int) and isinstance(b, int):
        return a - b
    if isinstance(a, str) and isinstance(b, str):
        return a + b
    return a + b

Let's test if the TypeError rises when no valid data is provided:

import pytest

from main import sum_instances

def test_cannot_add_int_and_str() -> None:
    with pytest.raises(TypeError):
        sum_instances(2, "3")


def test_cannot_add_2_lists() -> None:
    with pytest.raises(TypeError):
        # but it will not rise TypeError, because actually we can use ‘+’ with lists
        sum_instances([2], ["3"])  

The @pytest.mark.parametrize Decorator

Parametrization in the context of testing is a process of running the same test with different values from a prepared set. Each combination of a test and data is counted as a new test case. In the pytest for parametrization, we have the @pytest.mark.parametrize decorator.

Let’s test the same sum_instances function from the main.py using parametrization :

import pytest

from main import sum_instances


@pytest.mark.parametrize(
    "a,b,result",
    [
        ("Can", "Add", "CanAdd"),
        (2, 3, 5)
    ]
)
def test_can_sum(a: int | str, b: int | str, result: int | str) -> None:
    assert (
        sum_instances(a, b) == result
    ), f"Sum of {a} and {b} should be equal to {result}"

Now we can see the standard name of tests — test_can_sum[2-3-5], but it is not always clear and readable. We can provide custom test names in two ways.

The first one is by using the pytest.param before each group of parameters and adding the id as the last parameter:

@pytest.mark.parametrize(
    "a,b,result",
    [
        pytest.param("Can", "Add", "CanAdd", id="2 strings"),
        pytest.param(2, 3, 5, id="2 numbers")
    ]
)
def test_can_sum(a: int | str, b: int | str, result: int | str) -> None:
    assert (
            sum_instances(a, b) == result
    ), f"Sum of {a} and {b} should be equal to {result}"

The second one is by providing additional ids parameter for the @pytest.mark.parametrize:

import pytest

from main import sum_instances


@pytest.mark.parametrize(
   "a,b,result",
   [
       ("Can", "Add", "CanAdd"),
       (2, 3, 5)
   ],
   ids=[
       "2 strings",
       "2 numbers"
   ]
)
def test_can_sum(a: int | str, b: int | str, result) -> None:
   assert (
           sum_instances(a, b) == result
   ), f"Sum of {a} and {b} should be equal to {result}"