【问题标题】:Pytest patch fixture not resetting between test functions when using return_value使用 return_value 时,Pytest 补丁夹具不会在测试函数之间重置
【发布时间】:2021-12-28 06:31:03
【问题描述】:

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

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

夹具如下所示:

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

Entities 是我想要修补的一个类,但我希望它完全像原来的那样运行,因为它具有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

在第二种测试方法中,fixture 正在修补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/

这个插件提供了一个 mocker 固定装置,它是 mock 包提供的修补 API 的一个瘦包装器。除了在测试结束后自动撤消模拟之外,它还提供了其他不错的实用程序,例如spystub,并在比较调用时使用 pytest 内省。

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

【问题讨论】:

    标签: python mocking pytest pytest-mock


    【解决方案1】:

    您已经成为mutable default arguments 常见陷阱的受害者。每次设置 entities 属性时,实际上都会更改 entities 参数的默认值,因此下次将使用空参数创建新的 Entities 对象时,将使用它而不是空列表.

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

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

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

    【讨论】:

    • 这实际上是设计使然?
    • 是的。默认参数在加载时与函数对象一起存储,而不是在运行时重新创建。
    • 我添加了几个相关问题的链接。
    猜你喜欢
    • 2020-10-29
    • 2022-07-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-19
    • 1970-01-01
    • 2022-10-17
    • 1970-01-01
    相关资源
    最近更新 更多