I just started with mocks in python and I got to this use case and can't figure a working solution.
I want to return a MagicMock object from a class which was mocked using patch.
This is the folder structure:
.
├── tests
│ ├── __init__.py
│ └── test.py
└── utils
├── Creator.py
├── __init__.py
├── SecondLayer.py
└── User.py
2 directories, 6 files
SecondLayer.py
class SecondLayer:
def do_something_second_layer(self):
print("do_something_second_layer")
return 1
Creator.py
from utils.SecondLayer import SecondLayer
class Creator:
def get_second_layer(self):
second_layer = SecondLayer()
return second_layer
User.py
from utils.Creator import Creator
class User:
def __init__(self):
self.creator = Creator()
def user_do_something(self):
second_layer = self.creator.get_second_layer()
if second_layer.do_something_second_layer() == 1:
print("Returned 1")
else:
print("Returned 2")
And the test file:
import unittest
from unittest.mock import MagicMock, Mock, patch
from utils.User import User
# python3 -m unittest discover -p "*test*"
class TestUser(unittest.TestCase):
def setUp(self):
self.mock_second_layer = MagicMock(name="mock_second_layer")
config_creator = {
'get_second_layer.return_value': self.mock_second_layer}
self.creator_patcher = patch(
'utils.User.Creator', **config_creator)
self.mock_creator = self.creator_patcher.start()
self.user = User()
print(f'{self.mock_creator.mock_calls}')
def test_run_successful_run(self):
self.user.user_do_something()
# Does not prin calls to do_something_second_layer
print(f'self.mock_second_layer.mock_calls')
print(f'{self.mock_second_layer}')
print(f'{self.mock_second_layer.mock_calls}')
# Prints all calls also for the nested ones eg: get_second_layer().do_something_second_layer()
print(f'self.mock_creator.mock_calls')
print(f'{self.mock_creator}')
print(f'{self.mock_creator.mock_calls}')
def tearDown(self):
self.mock_creator.stop()
if __name__ == '__main__':
unittest.main()
When I run the tests, I receive this output:
$ python3 -m unittest discover -p "*test*"
[call()]
Returned 2
self.mock_second_layer.mock_calls
<MagicMock name='mock_second_layer' id='140404085721648'>
[call.__str__()]
self.mock_creator.mock_calls
<MagicMock name='Creator' id='140404085729616'>
[call(),
call().get_second_layer(),
call().get_second_layer().do_something_second_layer(),
call().get_second_layer().do_something_second_layer().__eq__(1),
call.__str__()]
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
As you can see: self.mock_second_layer.mock_calls
doesn't print the mock calls to do_something_second_layer
, it looks like it's not injected by the patch
call.
Can someone give me a solution on how can inject that self.mock_second_layer
via patch
and be able to then access the calls that were made on it? I tried for a few hours and I just can't get it working..
The problem comes from the fact that you provide the wrong (and quite exotic) keyword arguments to patch
via that config_creator
dictionary in setUp
.
What you are doing here is completely patching the Creator
class, replacing the class (this is important) with a MagicMock
instance, and then assigning the get_second_layer
attribute on that mock object yet another mock, which is instructed to return self.mock_second_layer
, when called.
This happens because of the way patch
works. The first argument is the target to be patched. In this case you tell it to patch the Creator
class.
Arbitrary keyword arguments are just passed along to the newly created mock to turn into its attributes. You provide **{'get_second_layer.return_value': self.mock_second_layer}
. By the way, this would not even be possible, if you tried to use regular keyword-argument notation because of the dot in the key.
Then, after the patch is applied, your Creator
class is that mock, you instantiate User
, which in turn calls Creator
in its constructor. Typically, this would instantiate a Creator
object, but since that is no longer a class at this point, it just calls your mock. Since you did not define a return value for that mock (just for its get_second_layer
attribute), it does what it does by default, namely returning yet another new MagicMock
object, and this is what is assigned to self.creator
inside User.__init__
.
There is nothing specified for that last mock. It is created on the fly. Any attribute access after that just results in the usual MagicMock
behavior, which is basically "sure I have this attribute, here ya go" and creates a another mock for that.
Thus, when you call user_do_something
in your test method, you just get a chain of generic mock calls that are all created on the fly.
You can actually see this happening, when you look at that last list of calls you provided:
call(),
call().get_second_layer(),
call().get_second_layer().do_something_second_layer(),
call().get_second_layer().do_something_second_layer().__eq__(1),
call.__str__()
The first one is the "instantiation" of Creator
(no arguments).
The rest are also all "on-the-fly"-created mock objects.
If you are now wondering, where your mock_second_layer
mock went, you can try a simple thing: Just add print(Creator.get_second_layer())
anywhere in User.__init__
for example. Notice that to get it, you need to omit the parentheses after Creator
.
If you really want to mock the entire Creator
class, you need to be careful to define what the mock replacing it will return because you are not using the class itself in your code, but instances of it. So you could set up a specific mock object that it returns, and then define its attributes accordingly.
Here is an example: (I put all your classes into one code
module)
from unittest import TestCase, main
from unittest.mock import MagicMock, patch
from . import code
class TestUser(TestCase):
def setUp(self):
self.mock_second_layer = MagicMock(name="mock_second_layer")
self.mock_creator = MagicMock(
name="mock_creator",
get_second_layer=MagicMock(return_value=self.mock_second_layer)
)
self.creator_patcher = patch.object(
code,
"Creator",
return_value=self.mock_creator,
)
self.creator_patcher.start()
self.user = code.User()
super().setUp()
def tearDown(self):
self.creator_patcher.stop()
super().tearDown()
def test_run_successful_run(self):
self.user.user_do_something()
print('\nself.mock_second_layer.mock_calls')
print(f'{self.mock_second_layer}')
print(f'{self.mock_second_layer.mock_calls}')
print('\nself.mock_creator.mock_calls')
print(f'{self.mock_creator}')
print(f'{self.mock_creator.mock_calls}')
Output:
Returned 2
self.mock_second_layer.mock_calls
<MagicMock name='mock_second_layer' id='...'>
[call.do_something_second_layer(),
call.do_something_second_layer().__eq__(1),
call.__str__()]
self.mock_creator.mock_calls
<MagicMock name='mock_creator' id='...'>
[call.get_second_layer(), call.__str__()]
Notice that when I start
the creator_patcher
, I don't capture its output. We don't need it here because it is just the mock replacing the class. We are interested in the instance returned by it, which we created beforehand and assigned to self.mock_creator
.
Also, I am using patch.object
, just because I find its interface easier to work with and more intuitive. You can still transfer this approach to regular patch
and it will work the same way; you'll just need to again provide the full path as a string instead of the target
and attribute
separately.
If you don't actually need to patch the entire class (because initialization is super simple and has no side effects), you could get away with just specifically patching the Creator.get_second_layer
method:
from unittest import TestCase, main
from unittest.mock import MagicMock, patch
from . import code
class TestUser(TestCase):
def setUp(self):
self.mock_second_layer = MagicMock(name="mock_second_layer")
self.get_second_layer_patcher = patch.object(
code.Creator,
"get_second_layer",
return_value=self.mock_second_layer,
)
self.mock_get_second_layer = self.get_second_layer_patcher.start()
self.user = code.User()
super().setUp()
def tearDown(self):
self.get_second_layer_patcher.stop()
super().tearDown()
def test_run_successful_run(self):
self.user.user_do_something()
print('\nself.mock_second_layer.mock_calls')
print(f'{self.mock_second_layer}')
print(f'{self.mock_second_layer.mock_calls}')
print('\nself.mock_get_second_layer.mock_calls')
print(f'{self.mock_get_second_layer}')
print(f'{self.mock_get_second_layer.mock_calls}')
Output:
Returned 2
self.mock_second_layer.mock_calls
<MagicMock name='mock_second_layer' id='...'>
[call.do_something_second_layer(),
call.do_something_second_layer().__eq__(1),
call.__str__()]
self.mock_get_second_layer.mock_calls
<MagicMock name='get_second_layer' id='...'>
[call(), call.__str__()]
This accomplishes essentially the same thing with a bit less code. But I would argue this is less "pure" in the sense that it technically does not fully decouple the User.user_do_something
test from Creator
. So I would probably still go with the first option.