Представьте, что ваш сервис обращается к внешнему API, например, чтобы получить информацию о погоде или текущий курс валют. Казалось бы, несложная задача. Однако реальность далека от идеала: серверы могут быть недоступны, запросы могут занимать много времени, а иногда вам просто не хочется тратить реальные деньги на API-лимиты. Тут-то и приходит на помощь мокирование.
Мокирование (mocking) — это процесс замены реальных зависимостей (например, внешнего API) поддельными объектами во время тестов. Это позволяет нам:
- Тестировать без зависимости от внешнего сервиса. Даже если сервис упал, тесты все равно пройдут.
- Ускорить выполнение тестов. Нет необходимости ждать ответа от сервера.
- Контролировать поведение внешнего API. Мы можем заранее задать ответы и эмулировать ошибки.
Как мокировать внешние API в Pytest?
В Python есть несколько популярных инструментов для мокирования, но сегодня мы сосредоточимся на двух подходах:
- Использование встроенного
unittest.mock— стандартный инструмент для создания моков. - Использование библиотеки
responses— специальный инструмент для мокирования HTTP-запросов.
Мокирование с помощью unittest.mock
Для начала давайте рассмотрим, как работает стандартный модуль unittest.mock. Он позволяет заменить реальные функции/объекты их поддельными версиями.
Пример: предположим, у нас есть метод, который обращается к API для получения данных о пользователе:
import httpx
async def get_user_data(user_id: int):
url = f"https://api.example.com/users/{user_id}"
async with httpx.AsyncClient() as client:
response = await client.get(url)
response.raise_for_status()
return response.json()
Теперь мы хотим протестировать этот метод, но не отправлять реальные запросы. Используем unittest.mock:
from unittest.mock import AsyncMock, patch
@pytest.mark.asyncio
async def test_get_user_data():
# Создаем фейковый ответ от API
mock_response = {"id": 1, "name": "John Doe"}
# Мокаем httpx.AsyncClient.get
with patch("httpx.AsyncClient.get", new_callable=AsyncMock) as mock_get:
mock_get.return_value.json.return_value = mock_response
mock_get.return_value.raise_for_status = AsyncMock()
# Вызываем нашу функцию
result = await get_user_data(1)
# Проверяем, что результат соответствует ожидаемому
assert result == mock_response
# Убедимся, что метод get вызывался с правильным URL
mock_get.assert_called_once_with("https://api.example.com/users/1")
Здесь мы использовали patch, чтобы заменить реальную функцию httpx.AsyncClient.get поддельной версией, которая возвращает заранее заданный результат.
Мокирование с помощью библиотеки responses
Для мокирования HTTP-запросов библиотека responses предоставляет удобный интерфейс для эмуляции ответов на запросы.
Прежде всего, установим библиотеку:
pip install responses
Пример: представим тот же сценарий, но теперь с использованием responses.
import responses
@responses.activate
def test_get_user_data_responses():
# Регистрируем фейковый ответ для конкретного URL
responses.add(
method=responses.GET,
url="https://api.example.com/users/1",
json={"id": 1, "name": "John Doe"},
status=200
)
# Вызываем нашу функцию (обычная синхронная версия для примера)
result = get_user_data_sync(1) # предположим, есть синхронная версия
# Проверяем результат
assert result == {"id": 1, "name": "John Doe"}
Обратите внимание, что с responses мы можем легко управлять не только ответами, но и статусами HTTP-запросов. Например, можно протестировать обработку 404 ошибки:
@responses.activate
def test_get_user_data_not_found():
responses.add(
method=responses.GET,
url="https://api.example.com/users/2",
status=404
)
with pytest.raises(Exception):
get_user_data_sync(2)
Сравнение подходов
| Особенность | unittest.mock |
responses |
|---|---|---|
| Гибкость | Высокая | Низкая |
| Простота использования для HTTP | Ниже (нужно много кода) | Намного проще |
| Встроенность | Стандартная библиотека Python | Требует установки |
| Управление ответами API | Требует настройки вручную | Интуитивно через методы add |
Если вы работаете исключительно с HTTP-запросами, responses может быть проще и удобнее. Если же нужно мокировать любые функции/методы, выбирайте unittest.mock.
Практика: тестирование внешнего API
Теперь давайте реализуем простой сервис, который получает данные о пользователях из внешнего API и обрабатывает их.
Приведём пример FastAPI-приложения:
from fastapi import FastAPI, HTTPException
import httpx
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: int):
url = f"https://api.example.com/users/{user_id}"
async with httpx.AsyncClient() as client:
response = await client.get(url)
if response.status_code == 404:
raise HTTPException(status_code=404, detail="User not found")
return response.json()
Тестирование с использованием responses
from fastapi.testclient import TestClient
import responses
client = TestClient(app)
@responses.activate
def test_get_user_success():
# Мокаем успешный ответ
responses.add(
method=responses.GET,
url="https://api.example.com/users/1",
json={"id": 1, "name": "John Doe"},
status=200
)
# Отправляем тестовый запрос
response = client.get("/users/1")
assert response.status_code == 200
assert response.json() == {"id": 1, "name": "John Doe"}
@responses.activate
def test_get_user_not_found():
# Мокаем 404 ошибку
responses.add(
method=responses.GET,
url="https://api.example.com/users/2",
status=404
)
# Отправляем тестовый запрос
response = client.get("/users/2")
assert response.status_code == 404
assert response.json() == {"detail": "User not found"}
С помощью responses мы легко протестировали как успешный случай, так и обработку ошибки 404.
Типичные ошибки при мокировании
Одной из самых частых ошибок является забывчивость. Например, вы забыли замокировать реальный API, и тест неожиданно зависает или падает. В таких случаях помогает использование фикстур и автоматизация мокирования.
Еще одна ошибка — чрезмерное использование моков. Помните, что мокирование полезно, но оно не заменит интеграционное тестирование с реальными сервисами.
Выводы
На практике мокирование внешних API помогает создавать надёжные системы и экономить ресурсы. Например, если ваш сервис делает сотни запросов к стороннему API, мокирование позволяет протестировать самые разные сценарии — от корректных ответов до непредвиденных ошибок.
Теперь, когда вы знаете основы и инструменты, вы готовы к созданию тестов для любых внешних API. В следующий раз, когда ваш интерфейс полетит в продакшен, вы будете знать: это не мы допустили ошибку, это просто "звёзды не сошлись".
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ