Как тестировать код, который использует глобальные переменные или одиночные экземпляры с помощью мок-объектов?

При тестировании кода с глобальными переменными или синглтонами, важно изолировать тестируемый модуль от нежелательных побочных эффектов.

Основные подходы:

  1. Использование unittest.mock (или pytest-mock): Мок-объекты позволяют заменить глобальные переменные/синглтоны временными "заглушками" в рамках теста.
  2. patch декоратор/контекстный менеджер: Удобный способ временно заменить атрибут модуля или класса моком. @patch('module.global_variable', new_value=mock_object)
  3. Рефакторинг (предпочтительнее): По возможности, избегать глобальных переменных и синглтонов. Инъекция зависимостей (dependency injection) делает код более тестируемым и гибким.
  4. Сброс состояния: Если невозможно избежать использования глобальных переменных/синглтонов, предусмотреть механизм для сброса их состояния после каждого теста (например, метод reset()).

Пример (с использованием unittest.mock):

import unittest
from unittest.mock import patch

# Модуль с глобальной переменной
import my_module

class TestMyModule(unittest.TestCase):
    @patch('my_module.global_variable', 'mocked_value')
    def test_my_function(self, mock_global_variable):
        # Теперь my_module.global_variable внутри теста - 'mocked_value'
        result = my_module.my_function()
        self.assertEqual(result, expected_result)
        # mock_global_variable - сам mock-объект, можно проверять вызовы:
        # mock_global_variable.assert_called_once()

Важно помнить о восстановлении исходного состояния после теста, чтобы избежать влияния на другие тесты.


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

1. Рефакторинг кода для внедрения зависимостей: Лучший подход - избегать прямого использования глобальных переменных или синглтонов внутри тестируемого кода. Вместо этого, передавайте эти зависимости как аргументы в функции или конструкторы классов. Это значительно упрощает мокирование.

# Вместо:
global_config = ...

def my_function():
  if global_config.debug_mode:
    print("Debug mode enabled")

# Используйте:
def my_function(config):
  if config.debug_mode:
    print("Debug mode enabled")

# Тогда в тесте:
from unittest.mock import MagicMock

mock_config = MagicMock()
mock_config.debug_mode = True
my_function(mock_config)

2. Мокирование глобальных переменных напрямую: Если рефакторинг невозможен, можно использовать unittest.mock.patch для временной замены глобальной переменной мок-объектом в рамках теста. Это работает только если глобальная переменная доступна для изменения (то есть, она не защищена от записи).

import unittest
from unittest.mock import patch

global_variable = ...

def function_using_global():
  return global_variable.some_method()

class MyTest(unittest.TestCase):
  @patch('your_module.global_variable') # Замените your_module на имя модуля
  def test_function_using_global(self, mock_global):
    mock_global.some_method.return_value = "mocked value"
    result = function_using_global()
    self.assertEqual(result, "mocked value")
    mock_global.some_method.assert_called_once()

3. Мокирование методов синглтона: Аналогично глобальным переменным, можно мокировать методы экземпляра синглтона с помощью patch.object. Сначала нужно получить экземпляр синглтона, а затем пропатчить нужный метод.

import unittest
from unittest.mock import patch

class Singleton:
  _instance = None

  def __new__(cls, *args, **kwargs):
    if not cls._instance:
      cls._instance = super().__new__(cls, *args, **kwargs)
    return cls._instance

  def get_data(self):
    return "Real data"

def function_using_singleton():
  singleton = Singleton()
  return singleton.get_data()


class MyTest(unittest.TestCase):
  @patch.object(Singleton, 'get_data')
  def test_function_using_singleton(self, mock_get_data):
    mock_get_data.return_value = "Mocked data"
    result = function_using_singleton()
    self.assertEqual(result, "Mocked data")
    mock_get_data.assert_called_once()

4. Контекстные менеджеры и фикстуры: Можно использовать контекстные менеджеры или фикстуры (например, в pytest) для автоматической замены глобальных переменных или синглтонов в начале теста и восстановления их исходного состояния по окончании. Это помогает избежать побочных эффектов между тестами.

Важно:

  • Будьте осторожны с мокированием глобальных переменных и синглтонов, так как это может сделать тесты хрупкими и сложными для понимания. Старайтесь минимизировать использование таких конструкций в коде.
  • Убедитесь, что тесты восстанавливают исходное состояние глобальных переменных/синглтонов после завершения, чтобы не влиять на другие тесты.
  • В некоторых случаях (особенно при работе с многопоточностью) прямое мокирование может быть ненадежным. В таких ситуациях рассмотрите возможность рефакторинга или использования более сложных техник тестирования, таких как интеграционные тесты.

В заключение, мокирование глобальных переменных и синглтонов возможно, но рекомендуется избегать прямого использования этих практик, если это возможно. Рефакторинг кода для внедрения зависимостей – лучший путь к более тестируемому и поддерживаемому коду.

0