DevGang
Авторизоваться

7 способов избежать проблем с Mock в тестах Python

Работа с unittest.mock в Python может превратиться в кошмар, когда тесты продолжают обращаться к сети или выдают AttributeError. «Ад Моков» замедляет тесты, делает их нестабильными и сложными в поддержке. Эта статья расскажет о важности мокирования для быстрых и надежных тестов и представит семь практических приемов для контроля зависимостей и поддержания «Здоровья Моков».

Взаимодействие с внешними сервисами (базами данных, API и т.д.) в тестах приводит к:

  1. Медленным тестам: из-за реальных операций ввода-вывода.
  2. Нестабильности: сетевые или файловые сбои влияют на результаты.
  3. Сложной отладке: `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

Патчите точное пространство имен:

  1. sub.function_one() -> патчите "my_module.sub.function_one".
  2. from mypackage.submodule import function_one -> патчите "my_module.function_one".

4. Изолируйте тесты, патча внешние вызовы

При обращении к внешним ресурсам (сеть, файлы, системные команды) используйте моки, чтобы:

  1. Избежать медленных и нестабильных операций в тестах.
  2. Тестировать только свой код, а не внешние зависимости.

Пример: чтение файла

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 – мощный инструмент. Можно пойти дальше:

  1. Альтернативные библиотеки: pytest-mock предлагает более удобный синтаксис.
  2. Автоматическая проверка моков: создайте инструмент для проверки соответствия моков импортам.
  3. Интеграционное тестирование: если моки скрывают слишком много, добавьте отдельные тесты для проверки реальных сервисов в контролируемой среде.

Попробуйте применить эти советы в следующем рефакторинге!

#Python #Testing
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу