Зачем использовать моки (mock) для внешних запросов в автотестах?

Ответ

Мокирование (подмена) внешних запросов в автотестах применяется для изоляции тестируемого модуля и создания стабильной, контролируемой тестовой среды. Это ключевая практика для модульного и интеграционного тестирования.

Основные причины для мокинга:

  • Независимость от внешних систем: Тесты не будут падать из-за недоступности стороннего API, изменений в его контракте или багов на той стороне.
  • Скорость: Исключаются сетевые задержки. Тесты выполняются на порядки быстрее.
  • Тестирование сложных сценариев: Позволяет легко эмулировать редкие или проблемные ситуации, которые сложно воспроизвести в реальной системе:
    • Ошибки сервера (500 Internal Server Error).
    • Таймауты.
    • Специфичные ответы (пустой список, нестандартный формат данных).
  • Отсутствие side-эффектов: Тесты не создают мусорных данных в реальной БД или продовольственном сервисе.
  • Детерминированность: Поведение мока предсказуемо, что делает тесты стабильными и воспроизводимыми.

Пример на Python с unittest.mock:

from unittest.mock import Mock, patch
import requests

def test_user_fetch_success():
    """Тест успешного получения данных пользователя."""
    # 1. Создаём мок-ответ, который имитирует успешный ответ API
    mock_response = Mock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"id": 1, "name": "John Doe"}

    # 2. Подменяем реальный `requests.get` нашим моком
    with patch('requests.get', return_value=mock_response) as mock_get:
        user_data = fetch_user_from_api(user_id=1) # Вызываем тестируемую функцию

        # 3. Проверяем, что функция вернула корректные данные
        assert user_data == {"id": 1, "name": "John Doe"}
        # 4. Проверяем, что запрос был выполнен с правильными параметрами
        mock_get.assert_called_once_with("https://api.example.com/users/1")

def test_user_fetch_404():
    """Тест обработки ошибки 404."""
    mock_response = Mock()
    mock_response.status_code = 404

    with patch('requests.get', return_value=mock_response):
        user_data = fetch_user_from_api(user_id=999)
        # Проверяем, что функция корректно обработала ошибку
        assert user_data is None

Важное замечание: Мокать следует с умом. Интеграционные и end-to-end тесты, которые проверяют взаимодействие с реальными системами, должны выполняться без моков (или с их минимальным использованием) на специально выделенных стендах.

Ответ 18+ 🔞

А, слушай, вот этот мокинг, блядь, в автотестах — это ж такая тема, просто ёперный театр! По сути, это когда ты берешь и подменяешь все эти внешние штуки, чтобы твой модуль тестировался в полной изоляции, как будто в вакууме, блядь. Создаешь себе такую стабильную, контролируемую песочницу, и там уже начинаешь издеваться над кодом.

А зачем, спрашивается, этот цирк нужен? Да овердохуища причин!

  • Независимость от левых систем: Твои тесты перестают падать из-за того, что какой-то API у соседей лег, контракт поменяли или у них просто пятница, и они накосячили. Твоё — твоё, ихнее — нахуй не надо.
  • Скорость, блядь! Выкидываем все эти сетевые задержки, ожидания ответа. Тесты начинают летать, как угорелые, в сотни раз быстрее.
  • Тестирование всякого пиздеца: Хочешь проверить, как твой код отреагирует на редкую ошибку? Легко! Эмулируй что угодно:
    • Сервер сгорел (500 ошибка).
    • Запрос завис навечно (таймаут).
    • Прилетел ответ, от которого волосы дыбом (пустой список, кривой JSON).
  • Никаких побочных эффектов: Тесты не насорят в реальную базу данных и не наделают заказов в продовольственном сервисе. Чистота, блядь!
  • Предсказуемость, ёпта! Поведение мока ты задал сам, значит, тест будет стабильным и воспроизводимым. Никаких сюрпризов.

Смотри, как это выглядит на Python с unittest.mock:

from unittest.mock import Mock, patch
import requests

def test_user_fetch_success():
    """Тест, когда всё прошло гладко."""
    # 1. Лепим муляж ответа, который притворяется успешным ответом от API
    mock_response = Mock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"id": 1, "name": "John Doe"}

    # 2. Подменяем реальный `requests.get` на нашу куклу
    with patch('requests.get', return_value=mock_response) as mock_get:
        user_data = fetch_user_from_api(user_id=1) # Дёргаем нашу функцию

        # 3. Сверяем, что функция не обделалась и вернула то, что надо
        assert user_data == {"id": 1, "name": "John Doe"}
        # 4. Убеждаемся, что запрос ушёл именно по тому адресу, куда мы хотели
        mock_get.assert_called_once_with("https://api.example.com/users/1")

def test_user_fetch_404():
    """Тест, когда пользователя не нашли."""
    mock_response = Mock()
    mock_response.status_code = 404 # Имитируем "не найдено"

    with patch('requests.get', return_value=mock_response):
        user_data = fetch_user_from_api(user_id=999)
        # Проверяем, что функция не сломалась, а корректно обработала провал
        assert user_data is None

Но, блядь, важный момент! Мокать надо с головой, а не через жопу. Интеграционные и end-to-end тесты, которые как раз и проверяют стыковку с реальным миром, должны гоняться без этих подмен (ну или с минимальным их количеством) на отдельных, специально отведённых для этого стендах. А то получится, что ты протестировал красивую сказку, а на деле всё накрылось медным тазом при первом же реальном запросе.