Как управлять состоянием мок-объектов между тестами?

Использовать `pytest fixtures` с областью видимости (scope), например, `function`, `module` или `session`, чтобы создать мок-объект и настроить его состояние. `function` создает новый мок для каждого теста, `module` - один для всех тестов в модуле, а `session` - один для всего тестового сеанса.
Для изменения состояния мока, можно использовать методы `fixture` или обратиться к моку напрямую внутри теста, в зависимости от области видимости и структуры кода. Важно сбрасывать или изменять состояние мока в teardown fixture (используя `yield` в fixture) если необходимо, чтобы избежать влияния на последующие тесты.

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

1. Использование фикстур с областью видимости (scope) "session" или "module" в pytest:

Фикстуры с областью видимости "session" создаются один раз для всей тестовой сессии, а фикстуры с областью видимости "module" создаются один раз для всего модуля. Это позволяет сохранять состояние мок-объекта между тестами в пределах сессии или модуля. В pytest это делается указанием параметра scope при определении фикстуры. Например:


    import pytest
    from unittest.mock import Mock

    @pytest.fixture(scope="module") # Или "session"
    def my_mock():
      mock = Mock()
      mock.some_attribute = 0  # Начальное состояние
      return mock

    def test_one(my_mock):
      my_mock.some_attribute += 1
      assert my_mock.some_attribute == 1

    def test_two(my_mock):
      assert my_mock.some_attribute == 1 # Состояние сохранено после test_one
      my_mock.some_attribute += 2
      assert my_mock.some_attribute == 3
  

Преимущества: Удобно для интеграционных тестов, где нужно эмулировать сохранение данных между операциями. Легко настраивается в pytest.

Недостатки: Может привести к запутанным зависимостям между тестами, если состояние мок-объекта изменяется непредсказуемо. Труднее понять порядок выполнения тестов и влияние каждого теста на состояние мока. Важно тщательно продумывать, когда использовать session или module scope.

2. Использование паттернов проектирования, таких как Singleton или Factory, для мок-объектов:

Если вам нужна гарантия, что все тесты используют один и тот же экземпляр мок-объекта, можно использовать Singleton. Factory позволит создавать экземпляры моков с определенными начальными состояниями.


    class MockSingleton:
      _instance = None

      def __new__(cls):
        if cls._instance is None:
          cls._instance = Mock()
          cls._instance.state = 0
        return cls._instance

    def test_one():
      mock = MockSingleton()
      mock.state += 1
      assert mock.state == 1

    def test_two():
      mock = MockSingleton()
      assert mock.state == 1  # Состояние сохранено
  

Преимущества: Более контролируемое управление состоянием. Централизованное место для инициализации и изменения состояния мок-объекта.

Недостатки: Требует дополнительного кода для реализации паттернов. Может быть сложнее для понимания, чем простые фикстуры. Singleton может нарушать принципы SOLID, если используется злоупотребление.

3. Сброс состояния мок-объекта в каждой функции setUp/tearDown (или фикстурах с scope "function"):

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


    import pytest
    from unittest.mock import Mock

    @pytest.fixture
    def my_mock():
      mock = Mock()
      mock.some_attribute = 0  # Начальное состояние
      yield mock
      mock.some_attribute = 0  # Сброс состояния после теста

    def test_one(my_mock):
      my_mock.some_attribute += 1
      assert my_mock.some_attribute == 1

    def test_two(my_mock):
      assert my_mock.some_attribute == 0  # Состояние сброшено
      my_mock.some_attribute += 2
      assert my_mock.some_attribute == 2
  

Преимущества: Уменьшает зависимость между тестами, упрощает отладку. Делает тесты более предсказуемыми и легкими для понимания.

Недостатки: Может быть менее удобным для интеграционных тестов, где нужно эмулировать сохранение состояния. Требует дополнительного кода для сброса состояния.

4. Использование отдельных мок-объектов для каждого теста:

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


    from unittest.mock import Mock

    def test_one():
      mock = Mock()
      mock.some_attribute = 0
      mock.some_attribute += 1
      assert mock.some_attribute == 1

    def test_two():
      mock = Mock()  # Новый мок-объект
      mock.some_attribute = 0
      mock.some_attribute += 2
      assert mock.some_attribute == 2
  

Преимущества: Полная изоляция, простота, минимальный риск влияния одного теста на другой.

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

Выбор подхода зависит от:

  • Типа тестирования (юнит, интеграционное, сквозное).
  • Сложности взаимодействия с мок-объектом.
  • Требований к изоляции тестов.
  • Необходимости эмулировать сохранение состояния.

Важно: Независимо от выбранного подхода, важно тщательно документировать состояние и поведение мок-объекта, чтобы тесты оставались понятными и поддерживаемыми. Необходимо избегать слишком сложных моков, которые имитируют слишком много поведения реального объекта. В таких случаях может быть полезнее использовать тестовые двойники (stubs) или фейковые объекты.

0