Зарегистрируйтесь, чтобы продолжить обучение

Мокинг Python: Продвинутое тестирование

В тестировании очень популярен мокинг. Технически он похож на стабинг, и из-за этого их часто путают. Но все же они служат разным целям и используются в разных ситуациях. В этом уроке мы разберемся, что такое мокинг и зачем он нужен.

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

В некоторых ситуациях требуется кое-что другое. Иногда нам нужно не получить результат работы функции, а проверить, выполняет ли она нужное нам действие — например, шлет правильный HTTP-запрос с правильными параметрами.

Для этого понадобятся моки. Они проверяют, как выполняется код.

HTTP

Начнем с такого фрагмента кода:

@pook.on
def test_get_private_fork_names():
    # Возвращается мок, за которым можно следить
    mock = pook.get(
        'https://api.github.com/orgs/hexlet/repos',
        reply=200,
        response_json=[{ 'fork': true, 'name': 'one' }, { 'fork': false, 'name': 'two' }]
    )

    get_private_fork_names('hexlet')
    # Убеждаемся, что вызов был сделан
    assert mock.calls == 1

Отслеживание выполнения какого-то действия — это и есть мокинг. Мок проверяет, что какой-то код выполнился определенным образом. Это может быть вызов функции, HTTP-запрос и тому подобное.

У мока две задачи:

  • Убедиться в том, что событие произошло — например, функция передала данные
  • Отследить, каким конкретно образом оно произошло — функция передала конкретные данные

Что дает нам такая проверка? В этом случае не очень много. Да, мы убеждаемся, что вызов был, но само по себе это еще ни о чем не говорит. В чем же тогда польза моков?

Представьте, что мы бы разрабатывали библиотеку PyGithub — ту самую, что выполняет запросы к GitHub API. Вся суть этой библиотеки в том, чтобы выполнить правильные запросы с правильными параметрами. Поэтому там нужно обязательно проверять выполнение запросов с указанием точных URL-адресов. Только в таком случае можно быть уверенными, что она выполняет верные запросы.

В этом ключевое отличие мока от стаба:

  • Стаб устраняет побочный эффект, чтобы не мешать проверке результата работы кода — например, возврату данных из функции
  • Мок фокусируется на том, как конкретно работает код, что он делает внутри

При этом чисто технически мок и стаб создаются почти одинаково, за исключением того, что на мок вешают ожидания, проверяющие вызовы. Из-за этого моками часто называют стабы. Для себя всегда пытайтесь понять, о чем идет речь. Это важно, потому что от этого зависит фокус тестов.

Функции

Моки довольно часто используют с функциями (методами). К примеру, они могут проверять:

  • Вызвана ли функция, сколько раз ее вызвали
  • Какие аргументы переданы в функцию, сколько всего аргументов
  • Что именно вернула функция

Предположим, что мы хотим протестировать функцию for_each(). Она вызывает колбек для каждого элемента коллекции:

for_each([1, 2, 3], lambda v : print(v))

Эта функция ничего не возвращает, поэтому напрямую ее не протестировать.

Можно попробовать сделать это с помощью моков. Проверим, что она вызывает переданный колбек и передает туда нужные значения. Сделаем это с помощью модуля mock, который входит в стандартную библиотеку Python как часть unittest:

from unittest.mock import Mock

def test_for_each():
    mock = Mock()

    for_each([1, 2, 3], lambda v : mock(v))

    # Проверяем, что она была вызвана с правильными аргументами нужное количество раз
    assert mock.call_count == 3
    mock.assert_any_call(1)
    mock.assert_any_call(2)
    mock.assert_any_call(3)

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

Можно сказать, что этот тест действительно проверяет работоспособность функции for_each(). Но можно сделать это проще, без мока и без завязки на внутреннее поведение. Для этого достаточно использовать замыкание:

def test_for_each():
    result = []
    numbers = [1, 2, 3]
    for_each(numbers, lambda x : result.append(x))
    assert result == numbers

Объекты

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

В тестировании объектов моки могут использоваться для имитации поведения объектов, от которых зависит тестируемый объект. Например, если объект A зависит от объекта B, то можно создать мок-объект для объекта B и использовать его в тестах объекта A. Это позволит тестировать объект A, не затрагивая объект B и его зависимости.

from unittest.mock import Mock

class MyObject:
    def __init__(self, dependency):
        self.dependency = dependency

    def my_method(self, arg):
        # какой-то метод, который зависит от зависимости
        return self.dependency.some_method(arg)

# Создаем мок-объект для зависимости
mock_dependency = Mock()

# Создаем объект, который мы будем тестировать
my_object = MyObject(mock_dependency)

# Имитируем поведение зависимости в мок-объекте
mock_dependency.some_method.return_value = 42

# Тестируем метод объекта
result = my_object.my_method("some argument")

# Проверяем, что метод объекта вызвал метод зависимости с правильным аргументом
mock_dependency.some_method.assert_called_once_with("some argument")

# Проверяем, что метод объекта вернул правильный результат
assert result == 42

Еще он умеет оборачивать существующую реализацию:

thing = ProductionClass()
thing.method = Mock(return_value=3)
thing.method(3, 4, 5, key='value')

thing.method.assert_called_with(3, 4, 5, key='value')

Преимущества и недостатки

Существуют ситуации, в которых моки нужны, но все таки в большинстве ситуаций их нужно избегать. Моки слишком много знают о том, как работает код. Любой тест с моками из черного ящика превращается в белый ящик.

Повсеместное использование моков приводит к двум вещам:

  • После рефакторинга приходится переписывать тесты, даже если код работает правильно. Так происходит, потому что тесты завязаны на то, как конкретно работает код
  • Код может перестать работать. При этом тесты будут проходить, потому что они сфокусированы не на результатах работы кода, а на его устройстве внутри

Там, где возможно использование реального кода — используйте реальный. Там, где возможно убедиться в работе кода без моков — делайте это без моков.

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


Дополнительные материалы

  1. unittest.mock
  2. Mock aren't stubs

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка веб-приложений на Django
10 месяцев
с нуля
Старт 2 января

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»