25

Im trying to patch multiple methods in a class. Here is my simplified set up

Hook.py is defined as

class Hook():
    def get_key(self):
        return "Key"

    def get_value(self):
        return "Value"

HookTransfer.py defined as

from Hook import Hook

class HookTransfer():
    def execute(self):
        self.hook = Hook()
        key = self.hook.get_key()
        value = self.hook.get_value()
        print(key)
        print(value)

I want to mock the methods get_key and get_value in the Hook class. The following works i.e. prints New_Key and New_Value

from HookTransfer import HookTransfer
import unittest
from unittest import mock

class TestMock(unittest.TestCase):
    @mock.patch('HookTransfer.Hook.get_key', return_value="New_Key")
    @mock.patch('HookTransfer.Hook.get_value', return_value="New_Value")
    def test_execute1(self, mock_get_key, mock_get_value):
        HookTransfer().execute()

if __name__ == '__main__':
    unittest.main()

However this does not. It prints <MagicMock name='Hook().get_key()' id='4317706896'> and <MagicMock name='Hook().get_value()' id='4317826128'>

from HookTransfer import HookTransfer
import unittest
from unittest import mock

class TestMock(unittest.TestCase):
    @mock.patch('HookTransfer.Hook', spec=True)
    def test_execute2(self, mock_hook):
        mock_hook.get_key = mock.Mock(return_value="New_Key")
        mock_hook.get_value = mock.Mock(return_value="New_Value")
        HookTransfer().execute()

if __name__ == '__main__':
    unittest.main()

Intuitively it seems like the second one should work too but it doesnt. Could you help explain why it does not. I suspect it has something to do with "where to patch" but Im unable to get clarity.

kvb
  • 625
  • 3
  • 8
  • 12
  • There is something inconsistent here ... In your first example, you `import HookTransfer` (which seems to be the name of the module), but then you _call_ the module in the test (as if you had `from HookTransfer import HookTransfer`). – mgilson Jan 13 '17 at 05:13
  • I hand wrote the code earlier because it was part of a much bigger file. I specifically tested and pasted the new code above. The result is the same. Thanks for pointing out. – kvb Jan 13 '17 at 07:24

3 Answers3

23

You can patch multiple methods of a module or a class using patch.multiple(). Something like this should work for your case:

import unittest
from unittest.mock import MagicMock, patch

class TestMock(unittest.TestCase):
    @patch.multiple('HookTransfer.Hook',
                    get_key=MagicMock(return_value='New_Key'),
                    get_value=MagicMock(return_value='New_Value'))
    def test_execute1(self):
        HookTransfer().execute()

If you use DEFAULT as the value for mocks, then patch.multiple() will create the mocks for you (MagicMocks by default). In this case the created mocks are passed into a decorated function by keyword, and a dictionary is returned when patch.multiple() is used as a context manager.

Eugene Yarmash
  • 142,882
  • 41
  • 325
  • 378
  • Using `Mock` rather than `MagicMock` seems to work fine, at least in my particular case. Could you say why you didn't use plain vanilla `Mock` here? – mike rodent Apr 15 '23 at 19:43
  • @mike I guess, it was simply because `MagicMock` is the default mock type used by [`patch.multiple`](https://docs.python.org/3/library/unittest.mock.html#patch-multiple).. In this case `Mock` should work as well as no magic methods need to be implemented by the mock. – Eugene Yarmash Apr 15 '23 at 20:28
14

What you need to is:

mock the class Hook,

from HookTransfer import HookTransfer
from Hook import Hook

import unittest
try:
    import mock
except ImportError:
    from unittest import mock

class TestMock(unittest.TestCase):
    @mock.patch.object(Hook, 'get_key', return_value="New_Key")
    @mock.patch.object(Hook, 'get_value', return_value="New_Value")
    def test_execute1(self, mock_get_value, mock_get_key):
        HookTransfer().execute()

if __name__ == "__main__":
    unittest.main()
Community
  • 1
  • 1
John He
  • 317
  • 2
  • 12
  • 6
    Correction: decorators are applied bottom-up, so the arguments on test_execute1 should be `(self, mock_get_value, mock_get_key)`. – Aldo Sep 01 '21 at 11:06
  • Thank you so much @Aldo for this little comment - I had a semi-related issue to this question but your comment saved the day, reminding me of how decorators are applied! – UrbanConor Oct 05 '21 at 17:24
5

After some testing I was able to find the issue.

In the second test case, the patch decorator creates a new instance of a Mock class and passes it via mock_hook argument to test_execute2 function. Lets refer to this as mock1. mock1 replaces the Hook class in HookTransfer.py. When self.hook = Hook() is run, it translates to calling __init__ of mock1. By design this returns yet another Mock instance - lets refer to this as mock2. So self.hook points to mock2. But mock_hook.get_key = mock.Mock(return_value="New_Key"), mocks the methods in mock1.

In order to mock correctly, mock2 needs to be patched. This can be done in 2 ways

  1. By mocking the return_value of mock1 (which returns mock2) mock_hook.return_value.get_key = mock.Mock(return_value="New_Key")
  2. Mocking the return value of constructor of mock1 (which returns mock2) mock_hook().get_key = mock.Mock(return_value="New_Key")

Under the wraps both options really do the same thing.

kvb
  • 625
  • 3
  • 8
  • 12
  • Shouldn't in the example the inputs be swapped? ``` {python} @mock.patch('HookTransfer.Hook.get_key', return_value="New_Key") @mock.patch('HookTransfer.Hook.get_value', return_value="New_Value") def test_execute1(self, mock_get_value, mock_get_key): ``` simliar to: https://stackoverflow.com/a/15922422/8320062 – Henrik Jun 08 '22 at 07:52