Издевательство в Python: unittest.mock, патч и ошибки, из-за которых тесты лгут

  1. Главная
  2. Блог
  3. Автоматизация тестирования

Автоматизация тестирования

Издевательство в Python: unittest.mock, патч и ошибки, из-за которых тесты лгут

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

8 мин чтения
Мокинг на Python: unittest.mock, патч и ошибки, из-за которых тесты лгут

Главное

  • mock заменяет реальный объект, например клиент API, поэтому тесты выполняются быстро и автономно.
  • Используйте return_value для фиксированного ответа и side_effect для ошибок или последовательности.
  • Исправьте имя там, где оно используется, а не там, где оно определено. Это самая распространенная ошибка.
  • Используйте spec или autospec, чтобы опечатки и неверные аргументы не прошли успешно, а не прошли молча.

Ваша функция вызывает API погоды. Ваш тест не должен зависеть от Интернета, ключа API или завтрашней погоды. Встроенная функция Python unittest.mock позволяет заменить этот вызов дублером, который возвращает именно то, что вы выберете.

Мы написали небольшой модуль weather и девять тестов, все из которых выполняются с помощью unittest Python 3.13. Все девять тестов прошли успешно, и приведенные ниже распечатанные результаты получены в результате этого запуска.

Зачем издеваться?

  • Скорость: никаких сетевых вызовов и настройки базы данных.
  • Надежность: тесты не завершаются неудачно из-за сбоя сторонней службы.
  • Управление: вы можете моделировать тайм-ауты, ошибки и нечетные данные по требованию.
  • Изоляция: вы тестируете свою логику, а не чужой сервер.

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

Имитация основ

from unittest.mock import Mock

api = Mock()
api.get_price.return_value = 499

print(api.get_price("pen"))                # 499
api.get_price.assert_called_once_with("pen")
print(api.get_price.call_args_list)        # [call('pen')]
print(api.anything_at_all())               # <Mock name='mock.anything_at_all()' ...>

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

MagicMock — та же идея, но также поддерживает «магические» методы, такие как __len__ и __iter__. patch() по умолчанию использует MagicMock.

Side_effect: ошибки и последовательности

side_effect управляет тем, что происходит при каждом вызове. Установите для него исключение, чтобы имитировать сбой:

api.fetch.side_effect = ConnectionError("down")
api.fetch()   # raises ConnectionError

Установите его в список, чтобы возвращать разные результаты при последовательных вызовах. Это идеально подходит для тестирования логики повторов:

def fetch_with_retry(api, attempts=3):
    for _ in range(attempts):
        try:
            return api.fetch()
        except ConnectionError:
            continue
    raise ConnectionError("all attempts failed")

api = Mock()
api.fetch.side_effect = [ConnectionError("busy"), {"ok": True}]
print(fetch_with_retry(api))    # {'ok': True}
print(api.fetch.call_count)     # 2

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

patch(): временно заменить что-то

Часто зависимость не передается. Она импортируется внутри модуля. patch() заменяет его на время теста и впоследствии восстанавливает:

# weather.py
from http_client import get as requests_get

def report(city):
    data = requests_get(f"https://api.example.com/weather?city={city}")
    return f"{city}: {data['temp_c']}°C"
from unittest.mock import patch

with patch("weather.requests_get") as fake_get:
    fake_get.return_value = {"temp_c": 31}
    print(weather.report("Pune"))   # Pune: 31°C
    fake_get.assert_called_once_with(
        "https://api.example.com/weather?city=Pune")

Вы можете использовать patch в качестве такого менеджера контекста или в качестве декоратора, который передает макет в качестве аргумента.

Патч там, где он используется, а не там, где он определен.

Эту ошибку однажды совершает почти каждый. Настоящая функция get находится в http_client, поэтому кажется естественным исправить ее там:

with patch("http_client.get") as fake_get:
    fake_get.return_value = {"temp_c": 99}
    weather.report("Pune")

# ConnectionError: no network in tests: https://api.example.com/weather?city=Pune

Настоящая функция работала. Когда weather.py выполнило from http_client import get as requests_get, оно создало собственное имя, указывающее на исходную функцию. Исправление http_client.get изменило другое имя, поэтому weather никогда не видел макета. Наш http_client.get-рейз умышленно, поэтому ошибка была громкой. В реальном проекте он может незаметно вызвать действующий API.

Правило: исправьте имя тестируемого модуля, записанное как "module_under_test.name". Если модуль выполнил import http_client и вызывает http_client.get(), то исправление "http_client.get" корректно, поскольку поиск происходит во время вызова.

Спецификация и автоспецификация: сделайте макеты строгими

Простой Mock принимает все, включая опечатки. В спецификации разрешены только атрибуты, которые есть у реального класса:

api = Mock(spec=weather.WeatherClient)
api.get_temprature("Pune")
# AttributeError: Mock object has no attribute 'get_temprature'

loose = Mock()
loose.get_temprature("Pune")    # returns a Mock, no error at all

Свободный макет с радостью принял метод с ошибкой, поэтому тест с его использованием мог пройти успешно, даже если реальный код потерпит крах. autospec идет дальше и проверяет сигнатуры методов:

with patch("weather.WeatherClient", autospec=True) as Fake:
    client = Fake.return_value
    client.get_temperature("Pune", "extra", "args")
# TypeError: too many positional arguments

Предпочитайте autospec=True или create_autospec(), когда высмеиваете свои собственные классы. Он сохраняет моки честными при изменении реального кода.

Издевательские даты и время

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

def today():
    return datetime.date.today()

def is_weekend():
    return today().weekday() >= 5

@patch("weather.today")
def test_date(self, fake_today):
    fake_today.return_value = datetime.date(2026, 11, 7)
    print(weather.is_weekend())   # True (a Saturday)

Вы не можете легко исправить datetime.date.today напрямую, поскольку встроенные типы не позволяют устанавливать свои атрибуты. Библиотеки, такие как Freegun и Time-Machine, справляются с этой задачей более полно.

Утверждения о издевательствах

Метод Чеки
assert_called() Позвонил хотя бы один раз
assert_called_once() Звонили ровно один раз
assert_called_with(...) Последний вызов использовал эти аргументы
assert_called_once_with(...) Ровно один вызов с этими аргументами
assert_any_call(...) Любой вызов использовал эти аргументы
assert_not_called() Никогда не звонил

Когда утверждение терпит неудачу, сообщение ясно. Мы вызвали assert_called_once() для метода, который никогда не запускался:

AssertionError: Expected 'save' to have been called once. Called 0 times.

Более старые версии Python позволяют использовать методы утверждения с ошибками, такие как assret_called_once, молча. Современные версии выдают ошибку для имен, начинающихся с assert или распространенных ошибок в написании, но спецификация по-прежнему является более сильной защитой.

pytest и издевается

Все вышеперечисленное работает и в pytest. Многие пользователи pytest также используют встроенное приспособление monkeypatch для простых замен или плагин pytest-mock, который предоставляет приспособление mocker, которое автоматически отменяет исправления. Ко всем из них применяется правило «патч там, где он используется». См. наше руководство по pytest для ознакомления с настройками и настройками.

Когда не стоит издеваться

  • Не высмеивайте то, что тестируете. Если макет делает всю работу, тест ничего не доказывает.
  • Избегайте издевательств над простыми объектами-значениями. Используйте настоящие вещи.
  • Не высмеивайте все подряд. Тесты, полные макетов, часто ломаются при рефакторинге кода, даже если поведение корректное.
  • Проведите несколько реальных интеграционных тестов. Моки кодируют ваши предположения о сервисе, и эти предположения могут быть ошибочными.

Моки также легко увеличивают покрытие кода, поскольку имитируемые пути выполняются без какого-либо реального поведения.

Распространенные ошибки в насмешках

  • Исправление неправильного имени. Описано выше, но по-прежнему является основной причиной того, что «мой макет игнорируется».
  • Забыли установить return_value. Затем тестируемый код получает Mock-объект вместо данных. Он может пройти прямо через вашу функцию и случайно пройти тест, например, когда вы только проверяете, что результат правдив.
  • Утверждение макета вместо результата. Проверка того, что save() был вызван, слабее, чем проверка того, что было сохранено. Используйте assert_called_once_with(...) с реальными аргументами или, что еще лучше, утверждайте результат функции.
  • Утечка патчей. Вызов patcher.start() без stop() оставляет подделку на месте для последующих тестов. Используйте контекстный менеджер, декоратор или addCleanup(patcher.stop).
  • Декораторы расположены в неправильном порядке. При наличии нескольких декораторов @patch нижний передается в качестве первого аргумента. Их смешивание незаметно настраивает неправильный макет.
  • Издевательство над библиотекой, которой вы не владеете во многих тестах. Оберните его в собственный небольшой класс и вместо этого высмеивайте его. Тогда ваши тесты описывают ваши потребности, а не API библиотеки.

Моки, заглушки и фейки

Эти слова часто используются свободно, но различия помогают при обсуждении тестов. заглушка просто возвращает готовые ответы. mock также записывает звонки, чтобы вы могли проверить, как он использовался. подделка — это работающая упрощенная реализация, например хранилище в памяти вместо базы данных. Python Mock может действовать как заглушка или макет. Фейки — это обычные классы, которые вы пишете сами, и они часто приводят к тестам, которые лучше выдерживают рефакторинг, поскольку проверяют поведение, а не точные вызовы.

Хорошее практическое правило — издеваться над краями вашей системы: сетью, часами, случайными числами, файловой системой и платными сторонними сервисами. Внутри этих краев отдавайте предпочтение реальным объектам. Тесты, построенные таким образом, остаются быстрыми и по-прежнему проверяют код, который вы на самом деле написали.

Итоги

Mocking — это точный инструмент: замените медленные или непредсказуемые зависимости, контролируйте, что они возвращают, и проверяйте, как ваш код их использует. Две привычки предотвращают большинство проблем. Исправьте имя в тестируемом модуле и используйте autospec, чтобы ваши макеты отклоняли вызовы, которые реальный объект отклонял бы.

Часто задаваемые вопросы

Что такое насмешка в Python?

Мокинг заменяет реальный объект, например API-клиент или базу данных, подделкой, которой вы управляете во время теста. Python включает это в модуль unittest.mock.

В чем разница между Mock и MagicMock?

MagicMock — это макет, который также поддерживает магические методы, такие как __len__, __iter__ и __enter__. patch() по умолчанию создает MagicMock.

Почему мой патч не работает?

Обычно потому, что вы исправили место, где объект определен, а не то, где он используется. Исправьте имя внутри тестируемого модуля.

В чем разница между return_value иside_effect?

return_value устанавливает фиксированный результат. Side_effect может вызвать исключение, вернуть значения из списка по порядку или вызвать функцию.

Что делает автоспецификация?

Он создает макет реального объекта, поэтому отсутствие атрибутов и неправильные аргументы вызывают ошибки, как и в реальном коде.

Продолжайте учиться

Работайте с Stack Quora

Нужна помощь с тестированием, WordPress или SEO?

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

Продолжайте читать

Больше руководств по тестирование, искусственный интеллект и современные разработки

Понравилась эта статья? Выберите тему и сразу приступайте к нашим новейшим руководствам.

Искусственный интеллект

Объяснение галлюцинаций искусственного интеллекта: почему языковые модели все выдумывают и как их уменьшить

Галлюцинации ИИ объяснили: что это такое, почему языковые модели их создают, распространенные типы, как уменьшить их с помощью заземления и подсказо…

Читать статью →
DevOps

Объяснение переменных среды: файлы .env, секреты и правильно выполненная конфигурация

Переменные среды объясняются на примерах Python, которые мы запускали: их чтение и установка, почему каждое значение является строкой, файлы .env, н…

Читать статью →
Разработка ПО

Объяснение ограничения скорости API: алгоритмы, заголовки и обработка ошибок 429

Ограничение скорости API объясняется с помощью проведенного нами моделирования: сегмент токенов, фиксированное окно, скользящее окно, 429 ответов, з…

Читать статью →
Автоматизация тестирования

Издевательство в Python: unittest.mock, патч и ошибки, из-за которых тесты лгут

Издевательство на Python с помощью unittest.mock, протестировано: Mock и return_value, Side_effect, исправление использования имени, спецификация и …

Читать статью →
Тестирование ПО

Объяснение покрытия кода: покрытие линий или ветвей и что на самом деле означает 100%

Покрытие кода объяснено с помощью реального запуска покрытия.py: покрытие строк, ветвей и условий, чтение отчета, почему 100%-ное покрытие все еще м…

Читать статью →
Базы данных и SQL

DELETE, TRUNCATE и DROP в SQL: различия, откат и когда использовать каждый из них

Объяснение DELETE, TRUNCATE и DROP на примере протестированного SQL: что каждый из них удаляет, откат, триггеры, сброс идентификаторов, скорость и ч…

Читать статью →
Базы данных и SQL

Объяснение представлений SQL: создавайте, используйте и избегайте ловушек, с примерами

Представления SQL, объясненные с помощью запросов, которые мы выполняли: создание представления, результаты в реальном времени, скрытие столбцов, пр…

Читать статью →
Разработка ПО

Интерфейс TypeScript и тип: реальные различия, протестированные с помощью компилятора

Интерфейс TypeScript против типа, проверенный реальным компилятором: что могут делать оба, что может делать только один, слияние объявлений, расшире…

Читать статью →
Разработка ПО

Объяснение Python *args и **kwargs: гибкие функции с понятными примерами

Python *args и **kwargs, объясненные с помощью кода, который мы запускали: упаковка и распаковка, порядок параметров, аргументы только по ключевым с…

Читать статью →
Разработка ПО

Лямбда-функции Python: синтаксис, реальное использование и когда их не следует использовать

Лямбда-функции Python, объясненные с помощью кода, который мы запускали: синтаксис, сортировка с помощью ключа, карты и фильтра, замыкания, ловушка …

Читать статью →
Искусственный интеллект

Что такое ИИ-агенты? Как они работают: можно запустить простой цикл агента

Что такое агенты ИИ? Простое руководство по циклу агента, инструментам, памяти и ограничениям, с небольшим циклом Python, который мы запустили, реал…

Читать статью →
DevOps

Мониторинг и наблюдаемость: реальная разница с журналами, метриками и трассировками

Объяснение мониторинга и наблюдаемости: известные и неизвестные сбои, журналы, метрики и трассировки, пошаговое руководство по инцидентам при оформл…

Читать статью →
DevOps

Docker Volumes против Bind Mounts: правильное сохранение данных контейнера

Объяснение томов Docker и монтирования привязки: почему исчезают данные контейнера, именованные тома, монтирование привязки, tmpfs, примеры создания…

Читать статью →
Автоматизация тестирования

TDD против BDD: ключевые различия, одна функция, созданная для обоих способов, и когда использовать каждый

TDD и BDD объясняются с помощью одной функции скидки, созданной в обоих направлениях: красно-зеленый рефакторинг с кодом, который мы запускали, сцен…

Читать статью →
Разработка ПО

Git merge vs rebase: отличия, примеры и когда что использовать

Git merge vs rebase с реальным выводом log, сравнительной таблицей, золотым правилом rebase и безопасным рабочим процессом для команды.

Читать статью →
Разработка ПО

Генераторы списков в Python: синтаксис, примеры и когда их избегать

Генераторы списков в Python: синтаксис, фильтры, if-else, вложенные циклы, измеренное время и случаи, когда обычный цикл for лучше.

Читать статью →
Базы данных и SQL

SQL JOIN: объяснение с примерами (Inner, Left, Right, Full)

Разберите SQL JOIN шаг за шагом на примерах таблиц и запросов: INNER, LEFT, RIGHT, FULL, CROSS и SELF, а также частые ошибки и советы по производите…

Читать статью →