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

Мок-объекты используются для имитации ошибок и исключений, перехватывая вызовы зависимостей и заменяя их контролируемым поведением. Например:

  
   from unittest.mock import patch, Mock
   import pytest

   def function_to_test(api_client):
       try:
           data = api_client.fetch_data()
           return process_data(data)
       except APIError as e:
           return f"Error: {e}"

   class APIError(Exception):
       pass

   def test_function_with_api_error():
       mock_api_client = Mock()
       mock_api_client.fetch_data.side_effect = APIError("Simulated API Error")

       result = function_to_test(mock_api_client)

       assert result == "Error: Simulated API Error"
  
  

Здесь side_effect позволяет моку бросать исключение, имитируя сбой API. pytest (или unittest) используются для организации и запуска теста, а assert подтверждает ожидаемое поведение.


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

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

Свойство side_effect мок-объекта может быть установлено в исключение, которое будет вызвано при каждом вызове мока.


import unittest
from unittest.mock import Mock

class MyClass:
    def my_method(self, value):
        # Предположим, что этот метод взаимодействует с внешним сервисом
        pass

class TestMyClass(unittest.TestCase):
    def test_my_method_raises_exception(self):
        # Создаем мок для метода, который мы хотим проверить на обработку исключений
        mock_method = Mock(side_effect=ValueError("Simulated error"))

        # Создаем экземпляр класса, содержащего метод, который мы мокаем
        instance = MyClass()
        instance.my_method = mock_method

        # Проверяем, что при вызове метода с моком возникает ожидаемое исключение
        with self.assertRaises(ValueError) as context:
            instance.my_method(10)

        self.assertEqual(str(context.exception), "Simulated error")
  

В этом примере side_effect устанавливается равным ValueError("Simulated error"). Когда instance.my_method(10) вызывается, вместо реального выполнения метода выбрасывается исключение ValueError. assertRaises проверяет, что именно это исключение и было выброшено, и что его сообщение соответствует ожидаемому.

2. Использование return_value и условной логики в side_effect:

Можно использовать side_effect с функцией, которая решает, возвращать ли значение или вызывать исключение, основываясь на аргументах вызова.


import unittest
from unittest.mock import Mock

class MyClass:
    def my_method(self, value):
        # Предположим, что этот метод взаимодействует с внешним сервисом
        pass

class TestMyClass(unittest.TestCase):
    def test_my_method_conditional_exception(self):
        def side_effect_func(value):
            if value < 0:
                raise ValueError("Value cannot be negative")
            return value * 2

        mock_method = Mock(side_effect=side_effect_func)

        instance = MyClass()
        instance.my_method = mock_method

        # Проверяем исключение при отрицательном значении
        with self.assertRaises(ValueError) as context:
            instance.my_method(-5)
        self.assertEqual(str(context.exception), "Value cannot be negative")

        # Проверяем нормальное выполнение при положительном значении
        result = instance.my_method(5)
        self.assertEqual(result, 10)
  

Здесь side_effect - это функция side_effect_func, которая принимает аргумент value. Если value отрицательное, она выбрасывает ValueError. В противном случае она возвращает value * 2. Это позволяет имитировать различные сценарии ошибок в зависимости от входных данных.

3. Мокирование контекстных менеджеров, вызывающих исключения:

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


import unittest
from unittest.mock import Mock, patch

class MyClass:
    def read_file(self, filename):
        with open(filename, 'r') as f:
            return f.read()

class TestMyClass(unittest.TestCase):
    @patch('__main__.open')  # Обратите внимание: мокируем встроенную функцию open
    def test_read_file_raises_exception(self, mock_open):
        mock_file = Mock()
        mock_file.read.side_effect = IOError("File not found")  # Имитируем ошибку чтения файла

        mock_open.return_value = mock_file  # open() возвращает мок-объект, который имитирует файл

        instance = MyClass()

        with self.assertRaises(IOError) as context:
            instance.read_file("nonexistent_file.txt")

        self.assertEqual(str(context.exception), "File not found")

  

В этом примере мы мокируем встроенную функцию open. mock_file.read.side_effect = IOError("File not found") заставляет метод read() мок-объекта "файла" выбрасывать исключение IOError. Затем мы проверяем, что при вызове instance.read_file("nonexistent_file.txt") выбрасывается ожидаемое исключение.

Важные замечания:

  • Используйте assertRaises: Для проверки исключений используйте self.assertRaises(ExceptionType) в unittest.
  • Проверяйте сообщения исключений: Важно проверять не только тип исключения, но и его сообщение, чтобы убедиться, что выброшено именно то исключение, которое ожидается.
  • Будьте осторожны с побочными эффектами: Убедитесь, что ваши моки не вызывают неожиданных побочных эффектов, которые могут замаскировать ошибки в вашем коде.
  • Не мокируйте то, что не нужно: Мокируйте только внешние зависимости, такие как базы данных, API или файловые системы. Старайтесь избегать мокирования внутренних компонентов вашего кода, чтобы не снижать ценность тестов.
0