Издевательство в Python: unittest.mock, патч и ошибки, из-за которых тесты лгут
Моки позволяют тестировать код без реальной сети, базы данных или часов. При небрежном использовании они также пропускают неработающий код. Вот как правильно использовать 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 может вызвать исключение, вернуть значения из списка по порядку или вызвать функцию.
Что делает автоспецификация?
Он создает макет реального объекта, поэтому отсутствие атрибутов и неправильные аргументы вызывают ошибки, как и в реальном коде.
Продолжайте учиться
Нужна помощь с тестированием, WordPress или SEO?
Мы помогаем командам выпускать лучшее программное обеспечение и развивать свои сайты. Расскажите нам, над чем вы работаете, и мы предложим четкий следующий шаг.
Больше руководств по тестирование, искусственный интеллект и современные разработки
Понравилась эта статья? Выберите тему и сразу приступайте к нашим новейшим руководствам.
Объяснение галлюцинаций искусственного интеллекта: почему языковые модели все выдумывают и как их уменьшить
Галлюцинации ИИ объяснили: что это такое, почему языковые модели их создают, распространенные типы, как уменьшить их с помощью заземления и подсказо…
Читать статью →DevOpsОбъяснение переменных среды: файлы .env, секреты и правильно выполненная конфигурация
Переменные среды объясняются на примерах Python, которые мы запускали: их чтение и установка, почему каждое значение является строкой, файлы .env, н…
Читать статью →Разработка ПООбъяснение ограничения скорости API: алгоритмы, заголовки и обработка ошибок 429
Ограничение скорости API объясняется с помощью проведенного нами моделирования: сегмент токенов, фиксированное окно, скользящее окно, 429 ответов, з…
Читать статью →Автоматизация тестированияИздевательство в Python: unittest.mock, патч и ошибки, из-за которых тесты лгут
Издевательство на Python с помощью unittest.mock, протестировано: Mock и return_value, Side_effect, исправление использования имени, спецификация и …
Читать статью →Тестирование ПООбъяснение покрытия кода: покрытие линий или ветвей и что на самом деле означает 100%
Покрытие кода объяснено с помощью реального запуска покрытия.py: покрытие строк, ветвей и условий, чтение отчета, почему 100%-ное покрытие все еще м…
Читать статью →Базы данных и SQLDELETE, TRUNCATE и DROP в SQL: различия, откат и когда использовать каждый из них
Объяснение DELETE, TRUNCATE и DROP на примере протестированного SQL: что каждый из них удаляет, откат, триггеры, сброс идентификаторов, скорость и ч…
Читать статью →Базы данных и SQLОбъяснение представлений SQL: создавайте, используйте и избегайте ловушек, с примерами
Представления SQL, объясненные с помощью запросов, которые мы выполняли: создание представления, результаты в реальном времени, скрытие столбцов, пр…
Читать статью →Разработка ПОИнтерфейс TypeScript и тип: реальные различия, протестированные с помощью компилятора
Интерфейс TypeScript против типа, проверенный реальным компилятором: что могут делать оба, что может делать только один, слияние объявлений, расшире…
Читать статью →Разработка ПООбъяснение Python *args и **kwargs: гибкие функции с понятными примерами
Python *args и **kwargs, объясненные с помощью кода, который мы запускали: упаковка и распаковка, порядок параметров, аргументы только по ключевым с…
Читать статью →Разработка ПОЛямбда-функции Python: синтаксис, реальное использование и когда их не следует использовать
Лямбда-функции Python, объясненные с помощью кода, который мы запускали: синтаксис, сортировка с помощью ключа, карты и фильтра, замыкания, ловушка …
Читать статью →Искусственный интеллектЧто такое ИИ-агенты? Как они работают: можно запустить простой цикл агента
Что такое агенты ИИ? Простое руководство по циклу агента, инструментам, памяти и ограничениям, с небольшим циклом Python, который мы запустили, реал…
Читать статью →DevOpsМониторинг и наблюдаемость: реальная разница с журналами, метриками и трассировками
Объяснение мониторинга и наблюдаемости: известные и неизвестные сбои, журналы, метрики и трассировки, пошаговое руководство по инцидентам при оформл…
Читать статью →DevOpsDocker Volumes против Bind Mounts: правильное сохранение данных контейнера
Объяснение томов Docker и монтирования привязки: почему исчезают данные контейнера, именованные тома, монтирование привязки, tmpfs, примеры создания…
Читать статью →Автоматизация тестированияTDD против BDD: ключевые различия, одна функция, созданная для обоих способов, и когда использовать каждый
TDD и BDD объясняются с помощью одной функции скидки, созданной в обоих направлениях: красно-зеленый рефакторинг с кодом, который мы запускали, сцен…
Читать статью →Разработка ПОGit merge vs rebase: отличия, примеры и когда что использовать
Git merge vs rebase с реальным выводом log, сравнительной таблицей, золотым правилом rebase и безопасным рабочим процессом для команды.
Читать статью →Разработка ПОГенераторы списков в Python: синтаксис, примеры и когда их избегать
Генераторы списков в Python: синтаксис, фильтры, if-else, вложенные циклы, измеренное время и случаи, когда обычный цикл for лучше.
Читать статью →Базы данных и SQLSQL JOIN: объяснение с примерами (Inner, Left, Right, Full)
Разберите SQL JOIN шаг за шагом на примерах таблиц и запросов: INNER, LEFT, RIGHT, FULL, CROSS и SELF, а также частые ошибки и советы по производите…
Читать статью →