Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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()
like image 677
Tagc Avatar asked Sep 04 '16 00:09

Tagc


1 Answers

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.

like image 200
Simeon Visser Avatar answered Sep 28 '22 09:09

Simeon Visser