使用 return_value 时,Pytest 补丁夹具不会在测试功能之间重置

Pytest patch fixture not resetting between test functions when using return_value

我的一个装置有问题,它正在做一个补丁,在测试调用之间没有重置。

fixture 基本上是一个包装对象的补丁,因此我可以断言它已被传递到另一个函数中。

夹具看起来像这样:

@pytest.fixture
def mock_entities(mocker: MockFixture) -> MagicMock:
    entities = Entities()
    namespace = f"{__name__}.{Entities.__name__}"
    return mocker.patch(namespace, return_value=entities)

Entities 是我想要修补的 class,但我希望它的功能完全像原来的那样,因为它也有 property 方法使用 __len__。它是在函数体中声明的,我需要模拟它的原因是因为我将它传递给另一个函数并且我想断言它已被正确传递。我最初尝试过“wraps=`,但我无法让它正常工作。

完整测试代码如下:

import pytest
from pytest_mock import MockFixture
from unittest.mock import MagicMock, PropertyMock
from typing import List
from pprint import pprint
from unittest.mock import patch

class Entities:

    _entities: List[dict] = []

    def __init__(self, entities: List[dict] = []):
        self._entities = entities

    @property
    def entities(self) -> List[dict]:
        return self._entities

    @entities.setter
    def entities(self, value: List[dict]):
        self._entities = value

    def append(self, value: dict):
        self._entities.append(value)

    def __len__(self) -> int:
        return len(self._entities)

class ApiClient:

    def get_values(self) -> List[dict]:
        # We get values from a API with a pager mechanism here
        pass

class EntitiesCacheClient:

    def get_values(self) -> Entities:
        # We get values from cache here
        pass

    def set_values(sel, values: Entities):
        # We set values to cache here
        pass

class EntityDataSource:

    _api_client: ApiClient = None
    _cache_client: EntitiesCacheClient = None

    def __init__(self) -> None:
        self._api_client = ApiClient()
        self._cache_client = EntitiesCacheClient()

    def get_entities(self) -> Entities:

        entities = self._get_entities_from_cache()
        if entities:
            return entities

        # I want to mock Entities, so that I can assert that it is passed in to the EntitiesCacheClient.set_values()
        entities = Entities()
        api_values = 1 
        while api_values:
            api_values = self._api_client.get_values()
            if not api_values:
                break

            for values in api_values:
                entities.append(values)

        if entities:
            self._save_entities_to_cache(entities)

        return entities

    def _get_entities_from_cache(self) -> Entities:
        return self._cache_client.get_values()

    def _save_entities_to_cache(self, entities: Entities):
        self._cache_client.set_values(entities)


@pytest.fixture
def mock_entities_cache_client(mocker: MockFixture) -> MagicMock:
    namespace = f"{__name__}.{EntitiesCacheClient.__name__}"
    return mocker.patch(namespace, autospec=True).return_value

@pytest.fixture
def mock_api_client(mocker: MockFixture) -> MagicMock:
    namespace = f"{__name__}.{ApiClient.__name__}"
    return mocker.patch(namespace, autospec=True).return_value

@pytest.fixture
def mock_entities(mocker: MockFixture) -> MagicMock:
    entities = Entities()
    namespace = f"{__name__}.{Entities.__name__}"
    return mocker.patch(namespace, return_value=entities)

def test_entity_data_source_entities(mock_entities_cache_client, mock_api_client, mock_entities):

    mock_entities_cache_client.get_values.return_value = None

    expected_entity_1 = {"id": 1, "data": "Hello"}
    expected_entity_2 = {"id": 2, "data": "World"}

    expected_entities_list = [
        expected_entity_1, expected_entity_2
    ]

    mock_api_client.get_values.side_effect = [
        [
            expected_entity_1,
            expected_entity_2,
        ],
        []
    ]

    entity_data_source = EntityDataSource()
    result: Entities = entity_data_source.get_entities()

    mock_entities_cache_client.set_values.assert_called_once_with(mock_entities.return_value)

    assert len(result.entities) == len(expected_entities_list)
    assert result.entities == expected_entities_list

def test_entity_data_source_entities_more_results(mock_entities_cache_client, mock_api_client, mock_entities):

    mock_entities_cache_client.get_values.return_value = None

    expected_entity_1 = {"id": 1, "data": "Hello"}
    expected_entity_2 = {"id": 2, "data": "World"}
    expected_entity_3 = {"id": 3, "data": "How"}
    expected_entity_4 = {"id": 4, "data": "Are"}
    expected_entity_5 = {"id": 5, "data": "You"}
    expected_entity_6 = {"id": 6, "data": "Doing?"}

    expected_entities_list = [
        expected_entity_1, expected_entity_2, expected_entity_3,
        expected_entity_4, expected_entity_5, expected_entity_6
    ]

    mock_api_client.get_values.side_effect = [
        [
            expected_entity_1,
            expected_entity_2,
            expected_entity_3,
            expected_entity_4,
            expected_entity_5,
        ],
        [expected_entity_6],
        []
    ]

    entity_data_source = EntityDataSource()
    result: Entities = entity_data_source.get_entities()

    mock_entities_cache_client.set_values.assert_called_once_with(mock_entities.return_value)

    assert len(result.entities) == len(expected_entities_list)
    assert result.entities == expected_entities_list

在第二种测试方法中,夹具正在修补 Entities 并且它有一个 return_value=Entities()(基本上)。然而,fixture/mock似乎保留了第一次测试的原始Entities,这意味着它在_entities中已经有2条记录,导致总共有8条记录而不是6条它应该有。

>       assert len(result.entities) == len(expected_entities_list)
E       assert 8 == 6
E         -8
E         +6

为什么会这样?我认为在使用 pyest-mockmocker 固定装置时,不需要重置模拟,因为它会为您处理

https://pypi.org/project/pytest-mock/

This plugin provides a mocker fixture which is a thin-wrapper around the patching API provided by the mock package. Besides undoing the mocking automatically after the end of the test, it also provides other nice utilities such as spy and stub, and uses pytest introspection when comparing calls.

这不会扩展到分配给 return_value 的对象吗?如果这不是正确的方法,我应该如何嘲笑 Entities

您是 mutable default arguments 常见陷阱的受害者。每次设置 entities 属性 时,实际上都会更改 entities 参数的默认值,因此下一次将创建一个带有空参数的新 Entities 对象, 这将被用来代替空列表。

通常的解决方法是使用不可变的占位符对象作为默认值:

    def __init__(self, entities: List[dict] = None):
        self._entities = entities or []

如果您对这个设计决策的原因感兴趣,可以查看这些相关问题:

  • "Least Astonishment" and the Mutable Default Argument
  • Good uses for mutable function argument default values?