Зачем мокать (mock) запросы в тестировании?

Ответ

Мокирование (подмена) запросов — это ключевая техника для создания изолированных, быстрых и стабильных тестов. Основные причины её использования:

  • Изоляция тестируемого модуля: Позволяет тестировать логику клиентского кода или сервиса, не завися от работы реального внешнего API, базы данных или другого микросервиса. Если тот «упал» или изменился, мои тесты всё равно будут проходить.
  • Тестирование сложных и редких сценариев: Я могу легко смоделировать ответы, которые сложно или невозможно получить от реального сервиса в тестовом окружении.
    • Ошибки сервера: 500 Internal Server Error, 503 Service Unavailable.
    • Нестандартные ответы: Очень большие данные, специфичные форматы, медленные ответы (таймауты).
    • Edge-кейсы: Ответ с пустым телом, невалидный JSON.
  • Ускорение выполнения тестов: Моки работают в памяти, что в сотни раз быстрее, чем реальные сетевые вызовы.
  • Предотвращение side-эффектов: Тест не создаст запись в реальной продакшн-БД или не отправит настоящее SMS-сообщение.

Практический пример (Python, pytest с pytest-mock): Допустим, у меня есть функция get_user_data(user_id), которая внутри делает HTTP-запрос. Я хочу протестировать её логику обработки ответа.

import pytest
import requests

# Функция, которую тестируем
def get_user_data(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    if response.status_code == 200:
        return response.json()["data"]
    elif response.status_code == 404:
        return None
    else:
        raise Exception("API Error")

# Сам тест с моком
def test_get_user_data_success(mocker):  # 'mocker' — фикстура из pytest-mock
    # 1. Создаём мок-ответ, который имитирует успешный API-ответ
    mock_response = mocker.Mock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"data": {"id": 123, "name": "Alice"}}

    # 2. Подменяем реальный `requests.get` на наш мок
    # Теперь при вызове requests.get() в функции вернётся mock_response
    mocker.patch("requests.get", return_value=mock_response)

    # 3. Вызываем тестируемую функцию
    result = get_user_data(123)

    # 4. Проверяем, что функция корректно обработала мок-ответ
    assert result == {"id": 123, "name": "Alice"}
    # Дополнительно можно проверить, что запрос был вызван с правильным URL
    # requests.get.assert_called_once_with("https://api.example.com/users/123")

def test_get_user_data_not_found(mocker):
    mock_response = mocker.Mock()
    mock_response.status_code = 404
    mocker.patch("requests.get", return_value=mock_response)

    result = get_user_data(999)
    assert result is None  # Проверяем обработку 404 ошибки

Важно помнить, что моки должны максимально точно имитировать поведение реального сервиса, и их стоит использовать для unit- и component-тестов. Для интеграционного и end-to-end тестирования предпочтительнее использовать реальные сервисы или их точные стабы (stubs).

Ответ 18+ 🔞

Слушай, ну это ж классика, ебать мои старые костыли! Мокирование — это когда ты такой: «А давайте-ка я сейчас этот внешний сервис, который вечно тормозит и падает, на свою подставную куклу заменю, чтобы не ебться с ним». Суть в том, чтобы твои тесты были изолированными, быстрыми и стабильными. А то без этого — терпения ноль, ебть.

Зачем это вообще нужно, спросишь? Ну, во-первых, изоляция. Ты тестируешь свою логику, а не то, жив ли сегодня какой-то левый API на другом конце света. Упал он — а твоим тестам похуй, они проходят, потому что работают с твоей подделкой. Красота же!

Во-вторых, *тестирование всякого пиздца**, который в реальной жизни получить — тот ещё геморрой. Хочешь проверить, как твой код отреагирует на ошибку 500? Или на ответ, который медленнее смерти черепахи? Или на такой кривой JSON, что хоть святых выноси? С моками — раз плюнуть. Задал нужный ответ и поехал дальше.

В-третьих, скорость. Моки в памяти работают, это в сотни раз быстрее, чем тащиться по сети до реального сервиса. Тесты за секунды пролетают, а не за минуты.

Ну и наконец, безопасность. Ты же не хочешь, чтобы твой тест случайно наделал записей в продакшен-базу или разослал всем клиентам тестовые смски? Вот и я о том же. Моки не оставляют следов.

Пример на пальцах (Python, pytest): Допустим, есть у тебя функция, которая тащит данные пользователя по API. А я хочу проверить, как она обрабатывает успешный ответ.

import pytest
import requests

# Функция, которую тестируем
def get_user_data(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    if response.status_code == 200:
        return response.json()["data"]
    elif response.status_code == 404:
        return None
    else:
        raise Exception("API Error")

# Сам тест с моком
def test_get_user_data_success(mocker):  # 'mocker' — фикстура из pytest-mock
    # 1. Лепим муляж ответа от API
    mock_response = mocker.Mock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"data": {"id": 123, "name": "Alice"}}

    # 2. Подменяем реальный `requests.get` на нашу подставу
    # Теперь когда в функции вызовут requests.get(), получится наш mock_response
    mocker.patch("requests.get", return_value=mock_response)

    # 3. Дёргаем нашу функцию
    result = get_user_data(123)

    # 4. Проверяем, что она не обделалась и правильно всё распарсила
    assert result == {"id": 123, "name": "Alice"}
    # Можно ещё проверить, что запрос был именно с тем URL, который нужен
    # requests.get.assert_called_once_with("https://api.example.com/users/123")

def test_get_user_data_not_found(mocker):
    mock_response = mocker.Mock()
    mock_response.status_code = 404
    mocker.patch("requests.get", return_value=mock_response)

    result = get_user_data(999)
    assert result is None  # Проверяем, что на 404 она возвращает None

Главное помнить — моки должны вести себя как близнецы реальных сервисов, а не как мартышк* с гранатой. И да, это оружие для юнит- и компонентных тестов. Для интеграционных проверок лучше уже что-то ближе к реальности использовать.