В сложных проектах модульные тесты имеют большое значение. Вот почему так важны качество кода и ремонтопригодность модульных тестов.

1) @patch.object — твой друг

У декоратора @patch есть некоторые трудности:

Пути объектов могут сбивать с толку в больших проектах Python. Иногда разрешение путей может создать трудности для модульных тестов.

Вы должны написать путь в декораторе @patch в виде строки. Если вы наберете путь с ошибкой, ваша IDE, скорее всего, не предупредит вас об этом.

Для каждой тестовой функции (также известной как тестовый пример) вы должны указать полный путь к фиктивному объекту.

Вы можете легко импортировать объекты через @patch.object.

Пример:

from unittest.mock import patch
@patch("folder1.folder2.file1.class1.func1")
def test_example(self, func1):
    pass
@patch("folder1.folder2.file1.class1.func1")
def test_example_2(self, func1):
    pass

vs.:

from unittest.mock import patch
from folder1.folder2.file1 import class1
@patch.object(class1, "func1")
def test_example(self, func1):
    pass
@patch.object(class1, "func1")
def test_example_2(self, func1):
    pass

2) Избегайте использования глобальных переменных

Использование глобальных переменных в модульных тестах может привести к неправильным результатам тестирования.

Если одна из ваших модульных тестовых функций изменяет глобальную переменную, другие ваши тестовые функции могут привести к неправильным результатам теста.

Предположим, у вас есть две функции:

def remove_from_list(name_list, name):
    name_list.remove(name)
    return name_list


def count_list(name_list):
    return len(name_list)

У вас будет два модульных теста:

names = ["Michael", "John"]
class ExampleTests(TestCase):

    def test_remove_from_list(self):
        self.assertEqual(remove_from_list(names, "Michael"), ["John"])

    def test_count_list(self):
        self.assertEqual(count_list(names), 2)

На первый взгляд они выглядят хорошо. Однако второй тест не пройдет:

FAILED tests/test_example.py::ExampleTests::test_count_list - AssertionError: 1 != 2

Потому что функция remove_from_list изменяет переменную names.

Поместим список names в функцию геттера (см.: метод мутатора):

def get_names():
    return ["Michael", "John"]


class ExampleTests(TestCase):

    def test_remove_from_list(self):
        self.assertEqual(remove_from_list(get_names(), "Michael"), ["John"])

    def test_count_list(self):
        self.assertEqual(count_list(get_names()), 2)

Оба испытания прошли.

Я мог бы создать неглубокую копию переменной имен:

# from copy import copy
def test_remove_from_list(self):
    self.assertEqual(remove_from_list(copy(names), "Michael"), ["John"])

Это не рекомендуемый способ передачи переменной в другую тестовую функцию. Потому что, когда вам нужно использовать список имен, вам нужно будет не забыть использовать функцию копировать.

3) Избавьтесь от ненужных фиктивных переменных

Когда вы создаете фиктивный объект, вы должны создать аргумент функции

Пример:

@patch.object(class1, "func1", return_value=["abc"])
def test_example(self, mock_func1):
    pass

Если вам не нужна переменная mock_func1 в тестовой функции, вы можете обернуть фиктивный объект следующим образом:

@patch.object(class1, "func1", Mock(return_value=["abc"]))
def test_example(self):
    pass

4) @patch.multiple — твой друг

Предположим, вам нужно будет создать фиктивный объект для двух разных методов класса. Вот для чего нужен @patch.multiple

Пример:

@patch.multiple(
    class1,
    get_name=Mock(return_value="Michael"),
    get_age=Mock(return_value=25),
)
def test_example(self):
    self.assertEqual("Michael", class1().get_name())
    self.assertEqual(25, class1().get_age())

5) Факер тоже твой друг

Faker — это пакет Python, который генерирует для вас поддельные данные. В моих предыдущих тестовых функциях я запускал свои тестовые функции с теми же строками (Майкл и Джон).

Благодаря Faker я мог каждый раз запускать тестовые функции со случайными именами.

Пример:

names = [faker.name(), faker.name()]