Что такое mock-объекты в Python и как они используются в тестировании с pytest?

Ответ

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

Основная библиотека для мокирования в Python — это unittest.mock, которая отлично интегрируется с pytest.

Зачем нужны моки?

  • Изоляция: Тестировать один компонент системы, не затрагивая другие.
  • Предсказуемость: Задавать конкретные возвращаемые значения или исключения, чтобы проверить все ветки кода.
  • Скорость: Избегать медленных операций, таких как сетевые запросы или работа с диском.
  • Проверка взаимодействий: Убедиться, что тестируемый код правильно вызывает методы зависимых объектов.

Пример использования с pytest и pytest-mock:

Плагин pytest-mock предоставляет удобную фикстуру mocker.

# a.py - модуль с функцией, которую мы хотим протестировать
import requests

def get_user_data(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    response.raise_for_status() # Вызовет исключение для плохих статусов
    return response.json()

# test_a.py - тест для функции
import pytest
import requests
from .a import get_user_data

def test_get_user_data_success(mocker):
    # 1. Создаем mock для ответа от requests.get
    mock_response = mocker.Mock()
    mock_response.json.return_value = {"id": 1, "name": "John Doe"}
    mock_response.raise_for_status.return_value = None # Указываем, что метод не должен вызывать исключение

    # 2. Подменяем (patch) метод requests.get
    mocker.patch('requests.get', return_value=mock_response)

    # 3. Вызываем нашу функцию
    user_data = get_user_data(1)

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

def test_get_user_data_failure(mocker):
    # Подменяем requests.get, чтобы он вызывал исключение
    mocker.patch('requests.get', side_effect=requests.exceptions.HTTPError)

    # Проверяем, что наша функция корректно обрабатывает исключение
    with pytest.raises(requests.exceptions.HTTPError):
        get_user_data(2)

В этом примере мы полностью контролируем поведение requests.get, не делая реального сетевого запроса.

Ответ 18+ 🔞

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

Вот представь: у тебя есть функция, которая полезла в интернет за данными. А интернет — он, сука, нестабильный, как твоё настроение по понедельникам. То ли ответит, то ли нет, то ли вообще тебе в рот чих-пых пришлёт. И как тестить такую функцию? Ждать, пока облака на небе правильно встанут? Не, чувак.

Вот тут и выходят на сцену моки — это такие подставные ублюдки, которые притворяются настоящими объектами. Их задача — изолировать твой код от всей этой внешней хуйни вроде баз данных, API или файловой системы. Чтобы ты мог проверить свою логику, а не стабильность интернета провайдера.

Зачем это, блядь, нужно?

  • Изоляция, ёбана! Чтобы тестить один кусок, а не всю систему разом. Как будто ты ремонтируешь двигатель, а не разбираешь всю тачку до винтика.
  • Предсказуемость. Ты сам решаешь, что этот подставной объект вернёт: данные, ошибку или просто послать тебя нахуй. Можно проверить все возможные сценарии.
  • Скорость. Не надо ждать, пока база данных почешется или API другого сервиса ответит. Всё происходит мгновенно, как пощёчина.
  • Проверка взаимодействий. Ты можешь убедиться, что твой код правильно тыкает палкой в другие объекты, с нужными аргументами и нужное количество раз.

Смотри, как это выглядит на практике с pytest и pytest-mock:

Плагин pytest-mock — это просто подарок судьбы, он даёт фикстуру mocker. Берёшь и пользуешься, как своей.

# a.py - вот этот модуль мы будем пытать
import requests

def get_user_data(user_id):
    # Эта функция лезет куда-то в интернет, а интернет — он ёбаный
    response = requests.get(f"https://api.example.com/users/{user_id}")
    response.raise_for_status() # Если статус плохой — будет скандал и исключение
    return response.json()

# test_a.py - а вот тут мы начинаем издеваться
import pytest
import requests
from .a import get_user_data

def test_get_user_data_success(mocker):
    # 1. Лепим муляж ответа от requests.get
    mock_response = mocker.Mock()
    # Говорим муляжу: "Когда у тебя вызовут .json(), верни вот это"
    mock_response.json.return_value = {"id": 1, "name": "John Doe"}
    # А метод raise_for_status пусть просто молча делает вид, что всё ок
    mock_response.raise_for_status.return_value = None

    # 2. Подменяем (patch) сам метод requests.get на нашу заглушку
    # Теперь когда кто-то вызовет requests.get, сработает наша подстава
    mocker.patch('requests.get', return_value=mock_response)

    # 3. Вызываем нашу родную функцию. Она даже не подозревает, что её кинули!
    user_data = get_user_data(1)

    # 4. Проверяем: получили ли мы то, что подсунули, и был ли вызов с правильными аргументами
    assert user_data == {"id": 1, "name": "John Doe"}
    # А вот это — магия проверки взаимодействий. Смотрит, вызывался ли наш подставной метод.
    requests.get.assert_called_once_with("https://api.example.com/users/1")

def test_get_user_data_failure(mocker):
    # А теперь давай устроим саботаж! Пусть requests.get сразу бросает исключение.
    mocker.patch('requests.get', side_effect=requests.exceptions.HTTPError)

    # Проверяем, что наша функция не сдрейфила и правильно прокидывает эту ошибку дальше
    with pytest.raises(requests.exceptions.HTTPError):
        get_user_data(2) # Вызываем функцию, ожидая, что сейчас будет БАБАХ!

Видишь, в чём прикол? Мы нихуя не ходили в интернет. Весь этот цирк происходил у нас в памяти. Мы полностью контролировали поведение requests.get. Это и есть сила моков, блядь! Ты избавляешься от непредсказуемости внешнего мира и тестируешь только свою логику. Красота, ёпта!