使用 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-mock
和 mocker
固定装置时,不需要重置模拟,因为它会为您处理
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?
我的一个装置有问题,它正在做一个补丁,在测试调用之间没有重置。
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-mock
和 mocker
固定装置时,不需要重置模拟,因为它会为您处理
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
andstub
, 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?