Pytest для джунов: Моки и интеграция
3 / 838%
Мокирование HTTP: requests-mock
25 минут
У вас есть класс GitHubClient который делает HTTP-запросы к GitHub API. Как протестировать БЕЗ реальных запросов к GitHub?
Цель: Научиться мокировать HTTP-запросы с помощью requests-mock.
Вы точно готовы?
Убедитесь, что умеете:
# Создавать моки и патчить зависимости
from unittest.mock import Mock, patch
@patch("module.requests")
def test_api(mock_requests):
mock_requests.get.return_value.json.return_value = {"data": 123}Если unittest.mock непонятен — вернитесь к unittest.mock основы.
Проблема: патчить requests неудобно
Код с HTTP-запросами
# src/github_client.py
import requests
class GitHubClient:
def __init__(self, token):
self.token = token
self.base_url = "https://api.github.com"
def get_user(self, username):
"""Получает данные пользователя"""
response = requests.get(
f"{self.base_url}/users/{username}",
headers={"Authorization": f"token {self.token}"}
)
return response.json()
def create_issue(self, repo, title, body):
"""Создаёт issue в репозитории"""
response = requests.post(
f"{self.base_url}/repos/{repo}/issues",
headers={"Authorization": f"token {self.token}"},
json={"title": title, "body": body}
)
return response.json()Тест с unittest.mock (многословно)
@patch("src.github_client.requests")
def test_get_user(mock_requests):
# ❌ Много кода для простого теста
mock_response = Mock()
mock_response.json.return_value = {
"login": "alice",
"name": "Alice Smith",
"public_repos": 42
}
mock_requests.get.return_value = mock_response
client = GitHubClient(token="fake-token")
user = client.get_user("alice")
assert user["login"] == "alice"Проблемы:
- ❌ Многословно (нужно создавать Mock, настраивать response)
- ❌ Сложно проверять query params, headers
- ❌ Не видно какой URL мокируется
Решение: requests-mock
Установка
pip install requests-mockБазовое использование
import requests
import requests_mock
def test_get_user():
with requests_mock.Mocker() as m:
# Мокируем GET запрос
m.get("https://api.github.com/users/alice", json={
"login": "alice",
"name": "Alice Smith",
"public_repos": 42
})
# Реальный вызов requests.get() попадёт в мок!
response = requests.get("https://api.github.com/users/alice")
data = response.json()
assert data["login"] == "alice"
assert data["public_repos"] == 42✅ Просто и понятно!
Использование как фикстура (рекомендуется)
import pytest
import requests_mock
@pytest.fixture
def mock_requests():
"""Фикстура для мокирования HTTP"""
with requests_mock.Mocker() as m:
yield m
def test_get_user(mock_requests):
"""Тест с фикстурой"""
mock_requests.get("https://api.github.com/users/alice", json={
"login": "alice",
"public_repos": 42
})
client = GitHubClient(token="fake-token")
user = client.get_user("alice")
assert user["login"] == "alice"Мокирование разных HTTP методов
def test_http_methods(mock_requests):
# GET
mock_requests.get("https://api.example.com/users", json={"users": []})
# POST
mock_requests.post("https://api.example.com/users", json={"id": 1})
# PUT
mock_requests.put("https://api.example.com/users/1", json={"updated": True})
# DELETE
mock_requests.delete("https://api.example.com/users/1", status_code=204)
# PATCH
mock_requests.patch("https://api.example.com/users/1", json={"patched": True})
# Любой метод
mock_requests.request("OPTIONS", "https://api.example.com/users", text="OK")Проверка запросов
Проверка headers
def test_authorization_header(mock_requests):
"""Проверяем что токен отправляется в заголовке"""
mock_requests.get("https://api.github.com/users/alice", json={"login": "alice"})
client = GitHubClient(token="secret-token")
client.get_user("alice")
# Проверяем что запрос был с правильным header
history = mock_requests.request_history[0]
assert history.headers["Authorization"] == "token secret-token"Проверка query parameters
def test_query_params(mock_requests):
"""Проверяем query параметры"""
mock_requests.get("https://api.github.com/search/repositories", json={
"items": []
})
# Делаем запрос с query params
response = requests.get("https://api.github.com/search/repositories", params={
"q": "pytest",
"sort": "stars"
})
# Проверяем что параметры отправлены
history = mock_requests.request_history[0]
assert history.qs["q"] == ["pytest"] # qs = query string
assert history.qs["sort"] == ["stars"]Проверка JSON body
def test_create_issue(mock_requests):
"""Проверяем JSON body в POST запросе"""
mock_requests.post("https://api.github.com/repos/user/repo/issues", json={
"id": 123,
"state": "open"
})
client = GitHubClient(token="token")
issue = client.create_issue("user/repo", "Bug report", "Something is broken")
# Проверяем отправленные данные
history = mock_requests.request_history[0]
assert history.json() == {
"title": "Bug report",
"body": "Something is broken"
}request_history: все запросы
def test_multiple_requests(mock_requests):
"""Проверяем несколько запросов"""
mock_requests.get("https://api.example.com/users/1", json={"id": 1})
mock_requests.get("https://api.example.com/users/2", json={"id": 2})
requests.get("https://api.example.com/users/1")
requests.get("https://api.example.com/users/2")
# История всех запросов
assert len(mock_requests.request_history) == 2
assert mock_requests.request_history[0].url == "https://api.example.com/users/1"
assert mock_requests.request_history[1].url == "https://api.example.com/users/2"Симуляция ошибок
HTTP ошибки (404, 500)
def test_user_not_found(mock_requests):
"""Тест обработки 404"""
# Мокируем 404 ответ
mock_requests.get("https://api.github.com/users/nonexistent", status_code=404)
client = GitHubClient(token="token")
with pytest.raises(requests.HTTPError):
client.get_user("nonexistent")
def test_server_error(mock_requests):
"""Тест обработки 500"""
mock_requests.get("https://api.github.com/users/alice", status_code=500)
client = GitHubClient(token="token")
with pytest.raises(requests.HTTPError):
client.get_user("alice")Timeout
import requests
def test_timeout(mock_requests):
"""Тест обработки timeout"""
# Симулируем timeout
mock_requests.get("https://api.github.com/users/alice", exc=requests.Timeout)
client = GitHubClient(token="token")
with pytest.raises(requests.Timeout):
client.get_user("alice")Connection error
def test_connection_error(mock_requests):
"""Тест обработки connection error"""
mock_requests.get("https://api.github.com/users/alice", exc=requests.ConnectionError)
client = GitHubClient(token="token")
with pytest.raises(requests.ConnectionError):
client.get_user("alice")Custom response body для ошибок
def test_api_error_message(mock_requests):
"""Проверяем обработку сообщения об ошибке"""
mock_requests.get("https://api.github.com/users/alice", status_code=403, json={
"message": "API rate limit exceeded"
})
client = GitHubClient(token="token")
try:
client.get_user("alice")
except requests.HTTPError as e:
error_data = e.response.json()
assert "rate limit" in error_data["message"].lower()URL matching: гибкая настройка
Exact match (точное совпадение)
def test_exact_url(mock_requests):
mock_requests.get("https://api.example.com/users/123", json={"id": 123})
# ✅ Работает
requests.get("https://api.example.com/users/123")
# ❌ Не работает (другой URL)
# requests.get("https://api.example.com/users/456")Regex match
import re
def test_regex_url(mock_requests):
# Мокируем любой URL вида /users/{id}
mock_requests.get(re.compile(r"https://api\.example\.com/users/\d+"), json={"ok": True})
# ✅ Оба работают
requests.get("https://api.example.com/users/123")
requests.get("https://api.example.com/users/456")ANY (любой URL)
import requests_mock
def test_any_url(mock_requests):
# Мокируем ЛЮБОЙ GET запрос
mock_requests.get(requests_mock.ANY, json={"fallback": True})
requests.get("https://api.example.com/anything")
requests.get("https://another-api.com/something")
# Все запросы попадут в этот мокПрактический пример: WeatherService
# src/weather_service.py
import requests
class WeatherService:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://api.openweathermap.org/data/2.5"
def get_current_weather(self, city):
"""Получает текущую погоду"""
response = requests.get(
f"{self.base_url}/weather",
params={"q": city, "appid": self.api_key, "units": "metric"}
)
response.raise_for_status()
return response.json()
def get_forecast(self, city, days=5):
"""Получает прогноз погоды"""
response = requests.get(
f"{self.base_url}/forecast",
params={"q": city, "appid": self.api_key, "units": "metric", "cnt": days}
)
response.raise_for_status()
return response.json()
# tests/test_weather_service.py
import pytest
import requests_mock
import requests
@pytest.fixture
def mock_requests():
with requests_mock.Mocker() as m:
yield m
def test_get_current_weather(mock_requests):
"""Тест получения текущей погоды"""
# Arrange: мокируем API
mock_requests.get("https://api.openweathermap.org/data/2.5/weather", json={
"main": {"temp": 15.5, "humidity": 80},
"weather": [{"description": "clear sky"}]
})
# Act
service = WeatherService(api_key="fake-key")
weather = service.get_current_weather("Moscow")
# Assert
assert weather["main"]["temp"] == 15.5
assert weather["weather"][0]["description"] == "clear sky"
# Verify request
history = mock_requests.request_history[0]
assert history.qs["q"] == ["Moscow"]
assert history.qs["appid"] == ["fake-key"]
assert history.qs["units"] == ["metric"]
def test_get_forecast(mock_requests):
"""Тест получения прогноза"""
mock_requests.get("https://api.openweathermap.org/data/2.5/forecast", json={
"list": [
{"dt": 1234567890, "main": {"temp": 15}},
{"dt": 1234567891, "main": {"temp": 16}},
{"dt": 1234567892, "main": {"temp": 14}},
]
})
service = WeatherService(api_key="fake-key")
forecast = service.get_forecast("Moscow", days=3)
assert len(forecast["list"]) == 3
assert forecast["list"][0]["main"]["temp"] == 15
def test_weather_api_error(mock_requests):
"""Тест обработки ошибки API"""
mock_requests.get(
"https://api.openweathermap.org/data/2.5/weather",
status_code=401,
json={"message": "Invalid API key"}
)
service = WeatherService(api_key="invalid-key")
with pytest.raises(requests.HTTPError) as exc_info:
service.get_current_weather("Moscow")
assert exc_info.value.response.status_code == 401
def test_weather_timeout(mock_requests):
"""Тест обработки timeout"""
mock_requests.get(
"https://api.openweathermap.org/data/2.5/weather",
exc=requests.Timeout
)
service = WeatherService(api_key="fake-key")
with pytest.raises(requests.Timeout):
service.get_current_weather("Moscow")✅ Все тесты работают мгновенно БЕЗ реального API!
Что вы изучили
- requests-mock — удобное мокирование HTTP
- HTTP методы — GET, POST, PUT, DELETE, PATCH
- Проверка запросов — headers, query params, JSON body
- request_history — история всех запросов
- Симуляция ошибок — 404, 500, timeout, connection error
- URL matching — exact, regex, ANY
Следующий урок
Отлично! Теперь вы умеете мокировать HTTP-запросы. Но что делать с временем и файлами?
Переходите к уроку 3: Мокирование времени и файлов
В следующем уроке вы узнаете:
freezegunдля мокирования datetime.now()- Тестирование кода с датами и таймаутами
mock_open()для file I/O без реальных файлов- Практические примеры
Устранение неисправностей
URL в моке не совпадает с URL в коде:
# ❌ Mock: /users, Code: /users/
mock_requests.get("https://api.example.com/users", ...)
requests.get("https://api.example.com/users/") # Не совпадает!
# ✅ Используйте regex для гибкости
mock_requests.get(re.compile(r"https://api\.example\.com/users/?"), ...)Используйте request_history[0].qs:
history = mock_requests.request_history[0]
assert history.qs["param"] == ["value"] # qs возвращает списки!Используйте with requests_mock.Mocker() или фикстуру с scope="function"
Используйте список ответов:
mock_requests.get("https://api.example.com/status", [
{"json": {"status": "pending"}},
{"json": {"status": "processing"}},
{"json": {"status": "done"}},
])
# Первый вызов вернёт "pending", второй "processing", третий "done"