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

Мокирование внешних ресурсов:
  1. Использовать библиотеку `unittest.mock` или `pytest-mock`.
  2. Создать мок-объект, имитирующий внешний ресурс. Например, `mock.MagicMock()` или `mock.patch()`.
  3. Заменить реальный объект моком в тестируемой функции. `mock.patch('module.external_resource', new=mock_object)`.
  4. Настроить поведение мока: Определить возвращаемые значения, исключения, побочные эффекты (`mock_object.return_value`, `mock_object.side_effect`).
  5. Запустить тестируемую функцию.
  6. Убедиться, что мок был вызван с ожидаемыми аргументами: `mock_object.assert_called_with()`, `mock_object.call_args`.

Пример (pytest):


    def my_function(api):
      # Использует api
      return api.get_data()

    def test_my_function(mocker):
      mock_api = mocker.MagicMock()
      mock_api.get_data.return_value = "test_data"
      result = my_function(mock_api)
      assert result == "test_data"
      mock_api.get_data.assert_called_once()
  

Цель: Изолировать функцию от внешних зависимостей, обеспечить предсказуемый результат теста.


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

Основные принципы использования мок-объектов:

  • Замена реальных объектов: Вместо использования реального соединения с базой данных или вызова API, мы создаем мок-объект, который имитирует поведение этого ресурса.
  • Контроль над поведением: Мы можем запрограммировать мок-объект на возврат определенных значений, вызов исключений или выполнение определенных действий в ответ на вызовы наших функций.
  • Проверка взаимодействия: Мы можем проверить, как функция взаимодействовала с мок-объектом: какие методы были вызваны, с какими аргументами, сколько раз и т.д.

Пример:

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


  import requests

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

Чтобы протестировать эту функцию, используя мок-объекты, можно воспользоваться библиотекой unittest.mock (встроена в Python) или pytest-mock (более удобная для использования с pytest):

Пример с unittest.mock:


  import unittest
  from unittest.mock import patch, MagicMock
  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()


  class TestGetUserData(unittest.TestCase):
      @patch('requests.get')
      def test_get_user_data_success(self, mock_get):
          # Настраиваем мок-объект
          mock_response = MagicMock()
          mock_response.json.return_value = {"id": 1, "name": "John Doe"}
          mock_response.raise_for_status.return_value = None  # Имитируем успешный ответ
          mock_get.return_value = mock_response

          # Вызываем функцию и проверяем результат
          user_data = get_user_data(1)
          self.assertEqual(user_data, {"id": 1, "name": "John Doe"})

          # Проверяем, что requests.get был вызван с правильным URL
          mock_get.assert_called_once_with("https://api.example.com/users/1")

      @patch('requests.get')
      def test_get_user_data_error(self, mock_get):
          # Настраиваем мок-объект для имитации ошибки
          mock_response = MagicMock()
          mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("Ошибка API")
          mock_get.return_value = mock_response

          # Проверяем, что функция вызывает исключение
          with self.assertRaises(requests.exceptions.HTTPError):
              get_user_data(1)

          # Проверяем, что requests.get был вызван с правильным URL
          mock_get.assert_called_once_with("https://api.example.com/users/1")

  if __name__ == '__main__':
      unittest.main()
  

Пояснения:

  • @patch('requests.get'): Этот декоратор заменяет реальную функцию requests.get мок-объектом. Мок-объект передается в качестве аргумента в тестовую функцию (mock_get).
  • MagicMock: Универсальный мок-объект, который позволяет имитировать любые методы и атрибуты.
  • mock_response.json.return_value = {"id": 1, "name": "John Doe"}: Указывает, что при вызове mock_response.json() мок-объект должен вернуть словарь с данными пользователя.
  • mock_response.raise_for_status.return_value = None: Указывает, что при вызове mock_response.raise_for_status(), который вызывается для проверки статуса ответа API, мок должен вернуть None (т.е. имитирует успешный ответ 200 OK).
  • mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("Ошибка API"): Указывает, что при вызове mock_response.raise_for_status(), мок-объект должен вызвать исключение requests.exceptions.HTTPError, имитируя ошибку от API.
  • mock_get.assert_called_once_with("https://api.example.com/users/1"): Проверяет, что функция requests.get была вызвана ровно один раз и с ожидаемым URL.

Преимущества использования мок-объектов:

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

Вывод:

Использование мок-объектов - важный навык для Python-разработчика. Это позволяет писать надежные, предсказуемые и быстрые тесты для функций, взаимодействующих с внешними ресурсами.

0