Иногда мы разрабатываем приложение, которое взаимодействует с внешними API (например, API для платежных систем, социальных сетей или курсов валют). Писать тесты для таких взаимодействий становится немного сложно, потому что:
- Внешний сервис может быть недоступен, когда вы тестируете.
- Результаты внешнего API могут быть непредсказуемыми (например, зависят от времени суток, данных пользователя или конверсии валют).
- Частые вызовы внешнего API могут быть дорогими или замедлять тестирование.
Mock-тестирование решает эту проблему. Оно позволяет "подменить" внешний API на фейковые данные во время выполнения тестов. Мы можем эмулировать (симулировать) поведение внешнего сервиса, возвращая предсказуемые ответы.
Немного терминологии
Mock (читается как "мок") — это объект, который подменяет другой объект. Он "прикидывается" реальным объектом или функцией и имитирует их поведение. В Python для Mock-тестирования используется библиотека unittest.mock, которая является частью стандартной библиотеки.
Основные принципы Mock-тестирования
Mock-тестирование основывается на следующих концепциях:
Подмена объектов. Во время тестирования вместо реального объекта (например, функции или класса, который делает HTTP-запросы) используется Mock-объект. Этот Mock-объект возвращает предсказуемые ответы.
Изоляция. Тестируемая функция изолируется от любых взаимодействий с внешним миром. Все действия, которые требуют внепрограммного взаимодействия, заменяются Mock-объектами.
Проверка вызовов. С помощью Mock-объектов можно проверять, какие методы были вызваны, с какими аргументами и в каком порядке.
Использование unittest.mock
Установка библиотеки
Спойлер — ничего устанавливать не нужно! Библиотека unittest.mock уже входит в стандартный комплект Python. Вы можете использовать ее на здоровье.
Основные инструменты unittest.mock Вот краткий список того, что нам понадобится для тестирования:
| Инструмент | Описание |
|---|---|
mock.Mock |
Создает объект-заглушку для подмены реального объекта |
mock.patch |
Контекстный менеджер или декоратор для подмены объектов |
mock.return_value |
Определяет значение, которое Mock-объект будет возвращать, если его вызвать |
mock.side_effect |
Позволяет Mock-объекту выполнять определенные действия при вызове (например, выбрасывать исключения) |
mock.call_count |
Показывает количество вызовов Mock-объекта |
mock.assert_called_with() |
Проверяет, что Mock-объект был вызван с заданными аргументами |
Пример: тестирование функции, взаимодействующей с внешним API
Предположим, мы пишем приложение, которое получает текущий курс валют. Вот наша функция:
import requests
def get_exchange_rate(base_currency, target_currency):
url = f"https://api.example.com/rates?base={base_currency}&target={target_currency}"
response = requests.get(url)
if response.status_code == 200:
return response.json().get("rate")
else:
raise Exception("Error while fetching exchange rate")
Без Mock-тестирования каждый раз при запуске теста мы будем отправлять реальный запрос к API. Это дорого, нестабильно и замедляет тесты. Поэтому мы используем Mock.
Тестирование с использованием mock.patch
Сначала подменим метод requests.get, чтобы он возвращал заранее заданные данные:
from unittest.mock import patch
import pytest
@patch("requests.get")
def test_get_exchange_rate(mock_get):
# Настраиваем mock-объект
mock_response = mock_get.return_value # Это имитирует возвращаемый объект response
mock_response.status_code = 200
mock_response.json.return_value = {"rate": 75.0}
# Вызываем функцию
rate = get_exchange_rate("USD", "RUB")
# Проводим проверки
assert rate == 75.0
mock_get.assert_called_once_with("https://api.example.com/rates?base=USD&target=RUB")
Что здесь происходит:
- Мы используем
@patchдля подмены вызоваrequests.getвнутри нашей функции. mock_get.return_valueпозволяет нам кастомизировать поведение возвращаемого объекта.- Мы проверяем, что наша функция корректно вызвала
requests.getс правильным URL. - Также мы проверяем, что результат работы функции соответствует ожиданиям.
Обработка исключений
Мы можем протестировать сценарий, когда API возвращает ошибочный HTTP-статус:
@patch("requests.get")
def test_get_exchange_rate_error(mock_get):
# Настраиваем mock-объект
mock_response = mock_get.return_value
mock_response.status_code = 500
# Вызываем функцию и ожидаем исключение
with pytest.raises(Exception, match="Error while fetching exchange rate"):
get_exchange_rate("USD", "RUB")
Пример: Подмена целого класса
Допустим, у нас есть класс CurrencyConverter, который обращается к API и преобразует сумму:
class CurrencyConverter:
def __init__(self, base_currency):
self.base_currency = base_currency
def convert(self, target_currency, amount):
rate = get_exchange_rate(self.base_currency, target_currency)
return rate * amount
Чтобы протестировать этот класс изолированно, мы можем подменить функцию get_exchange_rate:
@patch("__main__.get_exchange_rate")
def test_currency_converter(mock_get_exchange_rate):
# Настраиваем mock-объект
mock_get_exchange_rate.return_value = 75.0
# Создаем объект и проводим тест
converter = CurrencyConverter("USD")
result = converter.convert("RUB", 100)
# Проводим проверки
assert result == 7500.0
mock_get_exchange_rate.assert_called_once_with("USD", "RUB")
Использование mock.side_effect
Иногда вместо возврата фиксированного значения нужно сымитировать разные сценарии. Это можно сделать с помощью side_effect. Например:
@patch("requests.get")
def test_get_exchange_rate_with_side_effect(mock_get):
# Настраиваем side_effect для разных вызовов
def mock_json():
return {"rate": 75.0}
mock_response = mock_get.return_value
mock_response.status_code = 200
mock_response.json.side_effect = mock_json
# Тестируем
rate = get_exchange_rate("USD", "RUB")
# Проверяем
assert rate == 75.0
Или в случае выбрасывания исключения:
@patch("requests.get")
def test_get_exchange_rate_with_exception(mock_get):
mock_get.side_effect = Exception("API unavailable")
with pytest.raises(Exception, match="API unavailable"):
get_exchange_rate("USD", "RUB")
Практическое задание
Теперь ваша очередь. Напишите тест с использованием Mock-тестирования для функции, которая отправляет данные о заказе на сервер доставки. Примерный функционал:
def send_order_to_shipping(order_id, address):
url = f"https://api.example.com/shipping"
data = {"order_id": order_id, "address": address}
response = requests.post(url, json=data)
if response.status_code == 201:
return response.json().get("tracking_id")
else:
raise Exception("Shipping API error")
Попробуйте заменить requests.post, убедитесь, что данные отправляются корректно, и проверьте обработку ошибок.
Полезные ссылки
- Документация по
unittest.mock - pytest-mock — удобное расширение для Mock-тестирования с использованием pytest.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ