Skip to main content
Back to course
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"
Мокирование HTTP: requests-mock — Pytest для джунов: Моки и интеграция — Potapov.me