Tagc Tagc - 3 months ago 20
Python Question

Mock property return value gets overridden when instantiating mock object

Background



I'm trying to set up a test fixture for an application I'm writing in which one of my classes is replaced with a mock. I'm happy to leave most of the attributes of the mock class as the default
MagicMock
instances (where I'm only interested in making assertions about their usage), but the class also has a property that I want to provide a specific return value for.

For reference, this is the outline of the class I'm trying to patch:

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


Problem



The difficulty I'm having is that when I patch
CommunicationService
, I also try to set a
PropertyMock
for the
received_response
attribute that will return a specific value. When I instantiate this class in my production code, however, I'm finding that calls to
CommunicationService.received_response
are returning the default
MagicMock
instances instead of the specific value I want them to return.

During test setup, I do the following:

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)


In my production code (invoked after performing this setup):

# 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)


I expect
comms.received_response
to be an instance of
Subject
(the return value of the property mock). However, instead I get the following:

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


The problem seems to be that the mock property on the instance returned from the patch method works fine, but mock properties get messed up when creating a new instance of the patched class.

SSCCE



I believe that the snippet below captures the essence of this problem. If there's a way to modify the script below to make it so that
print(foo.bar)
returns
mock value
, then hopefully it'll show how I can resolve the problem in my actual code.

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()

Answer

The following line:

type(mock_foo).bar = mock_bar

mocks mock_foo which, at that point, is the return value of enter_context. If I understand the documentation correctly it means you're now actually handling the result of __enter__ of the return value of patch('__main__.Foo', spec=True).

If you change that line to:

type(Foo.return_value).bar = mock_bar

then you'll mock the property bar of instances of Foo (as the return value of calling a class is an instance). The second print statement will then print mock value as expected.