简体   繁体   English

如何从在 python 中使用补丁模拟的类的方法返回 MagicMock 对象

[英]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.我刚开始使用 python 中的模拟,我遇到了这个用例,但无法找到可行的解决方案。

I want to return a MagicMock object from a class which was mocked using patch.我想从使用补丁模拟的类中返回一个 MagicMock 对象。

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第二层.py

class SecondLayer:
    def do_something_second_layer(self):
        print("do_something_second_layer")
        return 1

Creator.py创建者.py

from utils.SecondLayer import SecondLayer

class Creator:
    def get_second_layer(self):
        second_layer = SecondLayer()
        return second_layer

User.py用户.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.如您所见: self.mock_second_layer.mock_calls没有打印对do_something_second_layer的模拟调用,看起来它不是由patch调用注入的。

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?有人可以给我一个解决方案,说明如何通过patch注入self.mock_second_layer并能够访问对其进行的调用吗? I tried for a few hours and I just can't get it working..我尝试了几个小时,但我无法让它工作..

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 .问题来自于您通过setUp中的config_creator字典提供了错误的(而且非常奇特的)关键字参数来patch

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.您在这里所做的是完全修补Creator类,用MagicMock实例替换(这很重要),然后在该模拟对象上分配get_second_layer属性另一个模拟,指示返回self.mock_second_layer ,调用时.

This happens because of the way patch works.发生这种情况是因为patch的工作方式。 The first argument is the target to be patched.第一个参数是要修补的目标 In this case you tell it to patch the Creator class.在这种情况下,您告诉它修补Creator类。

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} .您提供**{'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.然后,在应用补丁后,您的Creator类就是那个模拟,您实例化User ,它又在其构造函数中调用Creator Typically, this would instantiate a Creator object, but since that is no longer a class at this point, it just calls your mock.通常,这会实例化一个Creator对象,但由于此时它不再是一个类,它只是调用您的模拟。 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__ .由于您没有为该模拟定义返回值(仅针对其get_second_layer属性),它会默认执行它的操作,即返回另一个新的MagicMock对象,这就是在User.__init__中分配给self.creator的内容。

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.之后的任何属性访问只会导致通常的MagicMock行为,这基本上是“确定我有这个属性,这里你去”并为此创建另一个模拟。

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.因此,当您在测试方法中调用user_do_something时,您只会获得一系列动态创建的通用模拟调用。

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).第一个是Creator的“实例化”(无参数)。 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.如果你现在想知道你的mock_second_layer模拟去了哪里,你可以尝试一个简单的事情:例如,只需在User.__init__中的任何地方添加print(Creator.get_second_layer()) Notice that to get it, you need to omit the parentheses after Creator .请注意,要获得它,您需要省略Creator之后的括号。


Solution 1解决方案 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.如果你真的想模拟整个Creator类,你需要小心定义替换它的模拟将返回什么,因为你不是在你的代码中使用类本身,而是它的实例 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)这是一个例子:(我把你所有的类都放在一个code模块中)

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.请注意,当我start creator_patcher时,我没有捕获它的输出。 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 .我们对它返回的实例感兴趣,我们事先创建并分配给self.mock_creator

Also, I am using patch.object , just because I find its interface easier to work with and more intuitive.此外,我正在使用patch.object ,只是因为我发现它的界面更易于使用且更直观。 You can still transfer this approach to regular patch and it will work the same way;您仍然可以将这种方法转移到常规patch中,它的工作方式相同; you'll just need to again provide the full path as a string instead of the target and attribute separately.您只需要再次以字符串形式提供完整路径,而不是分别提供targetattribute


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:如果您实际上不需要修补整个类(因为初始化非常简单并且没有副作用),您可以只专门修补Creator.get_second_layer方法:

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 .但我认为这在技术上并没有完全将User.user_do_something测试与Creator分离的意义上不太“纯粹”。 So I would probably still go with the first option.所以我可能仍然会选择第一个选项。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM