Search code examples
pythonpython-3.xmockingpython-unittest

How to return MagicMock object from a method of a class that was mocked using patch in python


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..


Solution

  • Explanation

    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.


    Solution 1

    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.


    Solution 2

    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.