Mock-тестирование в Python: как изолировать код и улучшить тесты
Mock-тестирование в Python: как изолировать код и улучшить тесты
Mock-тестирование — это подход, при котором части системы заменяются «заглушками» (моками) для изоляции тестируемого кода от внешних зависимостей. В Python для этого используется модуль unittest.mock. В этой статье мы разберем, как применять моки на практике, и покажем примеры.
Зачем нужны моки?
- Изоляция тестов: Тестируйте код, не полагаясь на базы данных, API или сетевые вызовы.
- Контроль сценариев: Симулируйте любые условия (ошибки, задержки, специфические данные).
- Ускорение тестов: Избегайте долгих операций (например, реальных HTTP-запросов).
Основные инструменты: Mock, MagicMock, patch
1. Класс Mock
Объект-заглушка, который можно настраивать:
from unittest.mock import Mock
# Создаем мок-объект
http_client = Mock()
http_client.get.return_value = '{"status": "ok"}'
# Используем мок в тесте
result = http_client.get("https://api.example.com")
print(result) # {"status": "ok"}
2. MagicMock
Расширение Mock с поддержкой магических методов (например, __len__, __iter__):
from unittest.mock import MagicMock
mock_list = MagicMock()
mock_list.__len__.return_value = 5
print(len(mock_list)) # 5
3. Декоратор patch
Временно заменяет объект в заданном модуле:
from unittest.mock import patch
def call_external_api():
# Предположим, что здесь реальный HTTP-запрос
...
@patch('module_name.call_external_api')
def test_api_call(mock_api):
mock_api.return_value = "Mocked response"
result = call_external_api()
assert result == "Mocked response"
Пример 1: Тестирование функции с API-вызовом
Код для тестирования:
import requests
def fetch_data(url):
response = requests.get(url)
return response.json()
Тест с моком:
from unittest.mock import Mock, patch
@patch('requests.get')
def test_fetch_data(mock_get):
# Настраиваем мок
mock_response = Mock()
mock_response.json.return_value = {"data": "test"}
mock_get.return_value = mock_response
# Вызываем тестируемую функцию
result = fetch_data("https://api.example.com/data")
# Проверяем, что requests.get вызван с правильным URL
mock_get.assert_called_once_with("https://api.example.com/data")
# Проверяем результат
assert result == {"data": "test"}
Пример 2: Тестирование исключений
Код:
def process_file(filename):
try:
with open(filename, 'r') as f:
return f.read()
except FileNotFoundError:
return "File not found"
Тест:
from unittest.mock import mock_open, patch
@patch("builtins.open", new_callable=mock_open)
def test_process_file_error(mock_file):
# Симулируем исключение
mock_file.side_effect = FileNotFoundError
result = process_file("invalid.txt")
assert result == "File not found"
Полезные методы для работы с моками
- assert_called_with(): Проверка аргументов вызова.
- assert_not_called(): Убедиться, что метод не вызывался.
- side_effect: Задать исключение или функцию для динамического ответа.
- return_value: Зафиксировать возвращаемое значение.
Когда использовать моки?
- Работа с внешними сервисами (API, SMTP, базы данных).
- Тестирование исключительных сценариев (например, ошибка сети).
- Избежание побочных эффектов (чтобы тесты не меняли реальные данные).
Опасности моков
- Избыточное мокирование: Тесты могут стать хрупкими и неотражающими реальное поведение.
- Ложная уверенность: Моки могут маскировать проблемы интеграции между компонентами.
Best Practices
- Тестируйте поведение, а не реализацию: Не проверяйте, сколько раз вызвался мок, если это не критично.
- Используйте
autospec=Trueдля сохранения сигнатур оригинальных объектов:@patch('module.ClassName', autospec=True) - Комбинируйте с реальными тестами: Моки — для юнит-тестов, интеграционные тесты запускайте без них.
Пример 3: Моки в pytest (с плагином pytest-mock)
import pytest
def test_with_pytest_mock(mocker):
# Создаем мок для requests.get
mock_get = mocker.patch("requests.get")
mock_get.return_value.json.return_value = {"key": "value"}
result = fetch_data("https://example.com")
assert result == {"key": "value"}
Заключение
Mock-тестирование в Python — мощный инструмент для изоляции кода и создания надежных тестов. Используйте unittest.mock и pytest-mock, чтобы:
- Ускорять тесты.
- Контролировать зависимости.
- Тестировать крайние случаи.
Главное правило: Моки — не замена интеграционным тестам, а способ сделать юнит-тесты предсказуемыми и быстрыми.