Mock 属性 return 值在实例化模拟对象时被覆盖

Mock property return value gets overridden when instantiating mock object

背景

我正在尝试为我正在编写的应用程序设置测试夹具,其中我的 classes 之一被模拟替换。我很高兴将 mock class 的大部分属性保留为默认 MagicMock 实例(我只对它们的用法感兴趣),但 class 也有一个 属性,我想为其提供特定的 return 值。

作为参考,这是 class 我正在尝试修补的大纲:

class CommunicationService(object):
    def __init__(self):
        self.__received_response = Subject()

    @property
    def received_response(self):
        return self.__received_response

    def establish_communication(self, hostname: str, port: int) -> None:
        pass

    def send_request(self, request: str) -> None:
        pass

问题

我遇到的困难是,当我修补 CommunicationService 时,我还尝试为 received_response 属性设置一个 PropertyMock,这将 return 一个特定的价值。但是,当我在我的生产代码中实例化此 class 时,我发现对 CommunicationService.received_response 的调用是 return 默认 MagicMock 实例而不是我想要的特定值他们 return.

在测试设置期间,我执行以下操作:

context.mock_comms_exit_stack = ExitStack()
context.mock_comms = context.mock_comms_exit_stack.enter_context(
    patch('testcube.comms.CommunicationService', spec=True))

# Make 'received_response' observers subscribe to a mock subject.
context.mock_received_response_subject = Subject()
type(context.mock_comms).received_response = PropertyMock(return_value=context.mock_received_response_subject)

# Reload TestCube module to make it import the mock communications class.
reload_testcube_module(context)

在我的生产代码中(执行此设置后调用):

# Establish communication with TestCube Web Service.
comms = CommunicationService()
comms.establish_communication(hostname, port)

# Wire plugins with communications service.
for plugin in context.command.plugins:
    plugin.on_response = comms.received_response
    plugin.request_generated.subscribe(comms.send_request)

我希望 comms.received_response 成为 Subject 的实例(属性 模拟的 return 值)。但是,我得到以下信息:

<MagicMock name='CommunicationService().received_response' id='4580209944'>

问题似乎是实例 return 上的模拟 属性 从 patch 方法工作正常,但 mock 属性在创建新实例时搞砸了已修补 class.

SSCCE

我相信下面的代码片段抓住了这个问题的本质。如果有办法修改下面的脚本,使之成为 print(foo.bar) returns mock value,那么希望它能展示我如何在实际代码中解决问题。

from contextlib import ExitStack
from unittest.mock import patch, PropertyMock

class Foo:
    @property
    def bar(self):
        return 'real value'

exit_stack = ExitStack()
mock_foo = exit_stack.enter_context(patch('__main__.Foo', spec=True))
mock_bar = PropertyMock(return_value='mock value')
type(mock_foo).bar = mock_bar

print(mock_foo.bar) # 'mock value' (expected)

foo = Foo()
print(foo.bar) # <MagicMock name='Foo().bar' id='4372262080'> (unexpected - should be 'mock value')

exit_stack.close()

下面一行:

type(mock_foo).bar = mock_bar

mocks mock_foo,在这一点上,它是 enter_context 的 return 值。如果我正确理解 the documentation,则意味着您现在实际上正在处理 patch('__main__.Foo', spec=True).

的 return 值的 __enter__ 结果

如果将该行更改为:

type(Foo.return_value).bar = mock_bar

然后你将模拟 Foo 实例的 属性 bar(因为调用 class 的 return 值是一个实例) .第二个打印语句将按预期打印 mock value

这不是问题的答案,而是我从 Simeon 的答案中学到的对一个更简单问题的解决方案。

在我的例子中,我想模拟 obj.my_method().my_property 并得到 PropertyMock 作为 return,因为我直接设置 return 值的 属性到我的 PropertyMock 实例,而不是通过该方法编辑的模拟 return 的 type。这是固定代码:

with patch.object(MyObj, "my_method") as method_mock:
    property_mock = PropertyMock(return_value='foo')
    type(method_mock.return_value).my_property = property_mock