Как в Python изолировать unit-тест от внешних сетевых вызовов

Ответ

Unit-тесты по определению должны быть быстрыми, детерминированными и изолированными. Вызов реальных внешних ресурсов (API, базы данных) нарушает эти принципы. Для изоляции тестов от сетевых вызовов используется техника мокинг (mocking).

Основной инструмент в стандартной библиотеке Python — unittest.mock. Он позволяет заменить реальные объекты (функции, классы) на "заглушки" (моки) во время выполнения теста.

Пример с unittest.mock.patch:

Предположим, есть функция, которая обращается к внешнему API:

# my_module.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_my_module.py
from unittest.mock import patch
from my_module import get_user_data

# @patch заменяет 'requests.get' на мок-объект
@patch('my_module.requests.get')
def test_get_user_data(mock_get):
    # 1. Настраиваем мок: что он должен вернуть при вызове
    mock_response_data = {'id': 1, 'name': 'John Doe'}
    mock_get.return_value.status_code = 200
    mock_get.return_value.json.return_value = mock_response_data

    # 2. Вызываем тестируемую функцию
    user_data = get_user_data(1)

    # 3. Проверяем результат и то, что мок был вызван правильно
    mock_get.assert_called_once_with('https://api.example.com/users/1')
    assert user_data == mock_response_data

Преимущества этого подхода:

  • Скорость: Тест выполняется мгновенно, без реальной сетевой задержки.
  • Надежность: Результат теста не зависит от доступности API или состояния сети.
  • Контроль: Можно легко эмулировать любые сценарии, включая ошибки (например, status_code = 404), для проверки обработки исключений.

Для более удобной работы с HTTP-запросами также популярна библиотека responses.

Ответ 18+ 🔞

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

Так вот, чтобы не сойти с ума, умные дядьки придумали мокинг, ёпта. Суть проста, как три копейки: ты на время теста подменяешь реальную, ебанутую, зависимую от всего на свете хрень на свою управляемую заглушку. И тестируешь уже не «дошло ли письмо до Китая», а «правильно ли твоя функция пытается это письмо отправить».

В Питоне за это отвечает unittest.mock. Главный герой там — декоратор patch. Он как хирург: аккуратно вырезает кусок кода и вставляет туда свою железяку на время операции.

Смотри, вот у тебя есть функция, которая лезет за данными в какую-то левую апишку:

# my_module.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_my_module.py
from unittest.mock import patch
from my_module import get_user_data

# Смотри, какая магия, блядь! Говорим: «В этом тесте везде, где в my_module встречается requests.get, подставь нашу подделку»
@patch('my_module.requests.get')
def test_get_user_data(mock_get):
    # 1. Настраиваем нашу поддельную «ответку». Словно кукловод, блядь!
    mock_response_data = {'id': 1, 'name': 'John Doe'}
    mock_get.return_value.status_code = 200
    mock_get.return_value.json.return_value = mock_response_data

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

    # 3. А теперь проверяем, всё ли прошло по нашему сценарию.
    # Функция вызвала requests.get с правильным URL? Проверим!
    mock_get.assert_called_once_with('https://api.example.com/users/1')
    # И вернула ли она то, что мы ей подсунули?
    assert user_data == mock_response_data

И в чём, собственно, кайф, ёпта?

  • Скорость ебать! Тест отрабатывает за миллисекунды. Никаких ожиданий ответа от какого-то там сервера, который может в этот момент сосать лапу.
  • Стабильность, мать её! Результат теста зависит только от твоего кода, а не от фазы луны или настроения админа удалённого сервака.
  • Полный контроль, как в танке! Хочешь проверить, как твой код отреагирует на ошибку 404? Пожалуйста: mock_get.return_value.status_code = 404. На таймаут? Легко! Эмулируй любую пиздецовую ситуацию, не выходя из кабинета.

Есть, конечно, и другие библиотеки, вроде responses, которые для HTTP-запросов ещё удобнее могут быть. Но unittest.mock — это как швейцарский нож, всегда под рукой в стандартной библиотеке. Освой его, и половина твоих проблем с тестированием просто испарится, в рот меня чих-пых!