[英]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..我尝试了几个小时,但我无法让它工作..
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
之后的括号。
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.您只需要再次以字符串形式提供完整路径,而不是分别提供
target
和attribute
。
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.