7 способов избежать проблем с Mock в тестах Python
Работа с unittest.mock
в Python может превратиться в кошмар, когда тесты продолжают обращаться к сети или выдают AttributeError
. «Ад Моков» замедляет тесты, делает их нестабильными и сложными в поддержке. Эта статья расскажет о важности мокирования для быстрых и надежных тестов и представит семь практических приемов для контроля зависимостей и поддержания «Здоровья Моков».
Взаимодействие с внешними сервисами (базами данных, API и т.д.) в тестах приводит к:
- Медленным тестам: из-за реальных операций ввода-вывода.
- Нестабильности: сетевые или файловые сбои влияют на результаты.
- Сложной отладке: `AttributeError` и ложные срабатывания.
Надежные тесты важны для всех участников разработки. Случайные сбои и обращения к реальным сервисам нарушают CI/CD и замедляют разработку. Правильная изоляция зависимостей – общая задача.
Семь приемов, описанных ниже, помогут избежать «Ада Моков» и обеспечат простоту, точность и скорость тестов. Это своего рода чек-лист «Здоровья Моков».
1. Патчите там, где используется, а не где определено
Частая ошибка — патчить функцию в месте ее определения, а не в том модуле, где она вызывается. Python заменяет символы в тестируемом модуле, поэтому нужно открыть этот модуль и патчить именно там, где функция импортируется.
# my_module.py
from some.lib import foo
def do_things():
foo("hello")
Неверное определение: @patch("some.lib.foo")
Правильное использование: @patch("my_module.foo")
Это гарантирует замену my_module.foo
везде, где на нее ссылается тест.
2. Патчинг модулей и символов: что именно заменяется
Можно заменить отдельные функции/классы или весь модуль.
Патч символа: заменяет конкретную функцию/класс.
from unittest.mock import patch
with patch("my_module.foo") as mock_foo:
mock_foo.return_value = "bar"
Патч модуля: заменяет весь модуль на MagicMock. Все функции/классы внутри становятся моками.
with patch("my_module") as mock_mod:
mock_mod.foo.return_value = "bar"
# Remember to define every attribute your code calls
Если код обращается к другим атрибутам my_module
, их нужно настроить в mock_mod
, иначе будет AttributeError
.
3. Проверяйте фактический импорт, а не только трассировку
Трассировка может ввести в заблуждение относительно местоположения функции. Важно, как код ее импортирует. Всегда:
- Открывайте тестируемый файл (например,
my_module.py
). - Ищите строки импорта:
from mypackage.submodule import function_one
или
import mypackage.submodule as sub
Патчите точное пространство имен:
sub.function_one()
-> патчите"my_module.sub.function_one"
.from mypackage.submodule import function_one
-> патчите"my_module.function_one"
.
4. Изолируйте тесты, патча внешние вызовы
При обращении к внешним ресурсам (сеть, файлы, системные команды) используйте моки, чтобы:
- Избежать медленных и нестабильных операций в тестах.
- Тестировать только свой код, а не внешние зависимости.
Пример: чтение файла
def read_config(path):
with open(path, 'r') as f:
return f.read()
Патч в тестах:
from unittest.mock import patch
@patch("builtins.open", create=True)
def test_read_config(mock_open):
mock_open.return_value.read.return_value = "test config"
result = read_config("dummy_path")
assert result == "test config"
5. Уровень мокирования: высокий или низкий
Можно мокировать целые методы, работающие с внешними ресурсами, или отдельные вызовы библиотек. Выбор зависит от того, что нужно протестировать.
Патч высокого уровня:
class MyClass:
def do_network_call(self):
pass
@patch.object(MyClass, "do_network_call", return_value="mocked")
def test_something(mock_call):
# The real network call is never reached
...
Патч низкого уровня:
@patch("my_module.read_file")
@patch("my_module.fetch_data_from_api")
def test_something(mock_fetch, mock_read):
...
Высокоуровневые патчи проще настроить, но они не тестируют внутренние детали метода. Низкоуровневые дают больше контроля, но сложнее.
6. Назначайте атрибуты мок-модулям
При патчинге всего модуля он становится MagicMock()
без атрибутов. Если код вызывает:
import my_service
my_service.configure()
my_service.restart()
то в тестах нужно:
with patch("path.to.my_service") as mock_service:
mock_service.configure.return_value = None
mock_service.restart.return_value = None
...
Без этого будет ошибка:
AttributeError: Mock object has no attribute 'restart'
7. В крайнем случае, патчите вызывающую сторону высокого уровня
Если стек вызовов слишком сложен, можно пропатчить высокоуровневую функцию, чтобы код не доходил до глубоких импортов. Пример:
def complex_operation():
# This calls multiple external functions
pass
Если не нужно тестировать complex_operation
:
with patch("my_module.complex_operation", return_value="success"):
# No external dependencies get called
...
Это ускоряет тесты, но не тестирует внутренности complex_operation
.
Результаты и влияние
Систематическое применение этих стратегий «Здоровья Моков» дает следующие преимущества:
- Быстрые тесты: меньше операций ввода-вывода и сетевых запросов.
- Меньше скрытых ошибок: правильные моки уменьшают количество
AttributeError
. - Уверенность: стабильные и изолированные тесты обеспечивают надежные развертывания и удовлетворенность команды.
Команды, использующие эти практики, получают более надежные CI/CD. Разработчики меньше времени тратят на отладку нестабильных тестов и больше – на разработку.
+-----------------------------+
| Code Under Test |
| (Imports and Uses Mocked |
| Dependencies) |
+------------+----------------+
|
v
+-----------------------------+
| Patching Correct Namespace|
+-----------------------------+
|
v
+-----------------------------+
| Reduced Errors and Real I/O |
+-----------------------------+
- Code Under Test (Imports and Uses Mocked Dependencies) => Тестируемый код (Импорт и использование фиктивных зависимостей)
- Patching Correct Namespace => Исправление правильного пространства имен
- Reduced Errors and Real I/O => Уменьшение количества ошибок и реального ввода-вывода
Диаграмма показывает, как правильное мокирование перехватывает внешние вызовы и улучшает тесты.
Будущие направления
Моки в Python – мощный инструмент. Можно пойти дальше:
- Альтернативные библиотеки:
pytest
-mock предлагает более удобный синтаксис. - Автоматическая проверка моков: создайте инструмент для проверки соответствия моков импортам.
- Интеграционное тестирование: если моки скрывают слишком много, добавьте отдельные тесты для проверки реальных сервисов в контролируемой среде.
Попробуйте применить эти советы в следующем рефакторинге!