Уявіть собі, що ваш сервіс звертається до зовнішнього 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. Наступного разу, коли ваш інтерфейс полетить у продакшен, ви будете знати: це не ми припустилися помилки, просто «зірки не зійшлися».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ