JavaRush /Курсы /Модуль 3: Django /Mock-тестирование внешних API

Mock-тестирование внешних API

Модуль 3: Django
22 уровень , 7 лекция
Открыта

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

  • Внешний сервис может быть недоступен, когда вы тестируете.
  • Результаты внешнего API могут быть непредсказуемыми (например, зависят от времени суток, данных пользователя или конверсии валют).
  • Частые вызовы внешнего API могут быть дорогими или замедлять тестирование.

Mock-тестирование решает эту проблему. Оно позволяет "подменить" внешний API на фейковые данные во время выполнения тестов. Мы можем эмулировать (симулировать) поведение внешнего сервиса, возвращая предсказуемые ответы.

Немного терминологии

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

Основные принципы Mock-тестирования

Mock-тестирование основывается на следующих концепциях:

  • Подмена объектов. Во время тестирования вместо реального объекта (например, функции или класса, который делает HTTP-запросы) используется Mock-объект. Этот Mock-объект возвращает предсказуемые ответы.

  • Изоляция. Тестируемая функция изолируется от любых взаимодействий с внешним миром. Все действия, которые требуют внепрограммного взаимодействия, заменяются Mock-объектами.

  • Проверка вызовов. С помощью Mock-объектов можно проверять, какие методы были вызваны, с какими аргументами и в каком порядке.

Использование unittest.mock

Установка библиотеки

Спойлер — ничего устанавливать не нужно! Библиотека unittest.mock уже входит в стандартный комплект Python. Вы можете использовать ее на здоровье.

Основные инструменты unittest.mock Вот краткий список того, что нам понадобится для тестирования:

Инструмент Описание
mock.Mock Создает объект-заглушку для подмены реального объекта
mock.patch Контекстный менеджер или декоратор для подмены объектов
mock.return_value Определяет значение, которое Mock-объект будет возвращать, если его вызвать
mock.side_effect Позволяет Mock-объекту выполнять определенные действия при вызове (например, выбрасывать исключения)
mock.call_count Показывает количество вызовов Mock-объекта
mock.assert_called_with() Проверяет, что Mock-объект был вызван с заданными аргументами

Пример: тестирование функции, взаимодействующей с внешним API

Предположим, мы пишем приложение, которое получает текущий курс валют. Вот наша функция:

import requests

def get_exchange_rate(base_currency, target_currency):
    url = f"https://api.example.com/rates?base={base_currency}&target={target_currency}"
    response = requests.get(url)
    if response.status_code == 200:
        return response.json().get("rate")
    else:
        raise Exception("Error while fetching exchange rate")

Без Mock-тестирования каждый раз при запуске теста мы будем отправлять реальный запрос к API. Это дорого, нестабильно и замедляет тесты. Поэтому мы используем Mock.

Тестирование с использованием mock.patch

Сначала подменим метод requests.get, чтобы он возвращал заранее заданные данные:

from unittest.mock import patch
import pytest

@patch("requests.get")
def test_get_exchange_rate(mock_get):
    # Настраиваем mock-объект
    mock_response = mock_get.return_value  # Это имитирует возвращаемый объект response
    mock_response.status_code = 200
    mock_response.json.return_value = {"rate": 75.0}

    # Вызываем функцию
    rate = get_exchange_rate("USD", "RUB")

    # Проводим проверки
    assert rate == 75.0
    mock_get.assert_called_once_with("https://api.example.com/rates?base=USD&target=RUB")

Что здесь происходит:

  1. Мы используем @patch для подмены вызова requests.get внутри нашей функции.
  2. mock_get.return_value позволяет нам кастомизировать поведение возвращаемого объекта.
  3. Мы проверяем, что наша функция корректно вызвала requests.get с правильным URL.
  4. Также мы проверяем, что результат работы функции соответствует ожиданиям.

Обработка исключений

Мы можем протестировать сценарий, когда API возвращает ошибочный HTTP-статус:

@patch("requests.get")
def test_get_exchange_rate_error(mock_get):
    # Настраиваем mock-объект
    mock_response = mock_get.return_value
    mock_response.status_code = 500

    # Вызываем функцию и ожидаем исключение
    with pytest.raises(Exception, match="Error while fetching exchange rate"):
        get_exchange_rate("USD", "RUB")

Пример: Подмена целого класса

Допустим, у нас есть класс CurrencyConverter, который обращается к API и преобразует сумму:

class CurrencyConverter:
    def __init__(self, base_currency):
        self.base_currency = base_currency

    def convert(self, target_currency, amount):
        rate = get_exchange_rate(self.base_currency, target_currency)
        return rate * amount

Чтобы протестировать этот класс изолированно, мы можем подменить функцию get_exchange_rate:

@patch("__main__.get_exchange_rate")
def test_currency_converter(mock_get_exchange_rate):
    # Настраиваем mock-объект
    mock_get_exchange_rate.return_value = 75.0

    # Создаем объект и проводим тест
    converter = CurrencyConverter("USD")
    result = converter.convert("RUB", 100)

    # Проводим проверки
    assert result == 7500.0
    mock_get_exchange_rate.assert_called_once_with("USD", "RUB")

Использование mock.side_effect

Иногда вместо возврата фиксированного значения нужно сымитировать разные сценарии. Это можно сделать с помощью side_effect. Например:

@patch("requests.get")
def test_get_exchange_rate_with_side_effect(mock_get):
    # Настраиваем side_effect для разных вызовов
    def mock_json():
        return {"rate": 75.0}

    mock_response = mock_get.return_value
    mock_response.status_code = 200
    mock_response.json.side_effect = mock_json

    # Тестируем
    rate = get_exchange_rate("USD", "RUB")

    # Проверяем
    assert rate == 75.0

Или в случае выбрасывания исключения:

@patch("requests.get")
def test_get_exchange_rate_with_exception(mock_get):
    mock_get.side_effect = Exception("API unavailable")

    with pytest.raises(Exception, match="API unavailable"):
        get_exchange_rate("USD", "RUB")

Практическое задание

Теперь ваша очередь. Напишите тест с использованием Mock-тестирования для функции, которая отправляет данные о заказе на сервер доставки. Примерный функционал:

def send_order_to_shipping(order_id, address):
    url = f"https://api.example.com/shipping"
    data = {"order_id": order_id, "address": address}
    response = requests.post(url, json=data)
    if response.status_code == 201:
        return response.json().get("tracking_id")
    else:
        raise Exception("Shipping API error")

Попробуйте заменить requests.post, убедитесь, что данные отправляются корректно, и проверьте обработку ошибок.

Полезные ссылки

1
Задача
Модуль 3: Django, 22 уровень, 7 лекция
Недоступна
Замена вызова внешнего API
Замена вызова внешнего API
1
Задача
Модуль 3: Django, 22 уровень, 7 лекция
Недоступна
Обработка ошибок при запросе
Обработка ошибок при запросе
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ