模拟类方法中使用的 open() 函数
问题描述
我试图模拟在我的类的方法中使用的 open 函数.我发现这个线程 如何模拟一个在 with 语句中使用的 open(使用 Python 中的 Mock 框架)? 但无法解决我的问题.unittest 文档还显示了一个解决方案,它也没有模拟我打开的 https://docs.python.org/3/library/unittest.mock-examples.html#patch-decorators
I tried to mock the open function used in a method of my class. I found this thread How do I mock an open used in a with statement (using the Mock framework in Python)? but could not solve my issue. Also the unittest documention shows a solution which also didn't mock my open https://docs.python.org/3/library/unittest.mock-examples.html#patch-decorators
这是我的类,其中使用了open函数的方法:
This is my class with the method where the open function is used:
#__init.py__
import json
class MyClass:
def save_data_to_file(self, data):
with open('/tmp/data.json', 'w') as file:
json.dump(data, file)
...
mc = MyClass()
现在我找到了一些不同的解决方案.这是我的测试:
Now I found a little different solution. This is my test:
#save_to_file_test.py
from mymodule import MyClass
from mock import mock_open, patch
import ast
class SaveToFileTest(unittest.TestCase):
def setUp(self):
self.mc = MyClass()
self.data = [
{'id': 5414470, 'name': 'peter'},
{'id': 5414472, 'name': 'tom'},
{'id': 5414232, 'name': 'pit'},
]
def test_save_data_to_file(self):
m = mock_open()
with patch('mymodule.open', m, create=True):
self.mc.save_data_to_file(self.data)
string = ''
for call in m.return_value.write.mock_calls:
string += (call[1][0])
list = ast.literal_eval(string)
assertEquals = (list, self.data)
我不确定这是否是测试应写入文件的内容的最佳方法.当我测试 mock_calls(call_args_list 相同)时,这是传递给文件句柄的参数.欢迎任何意见、改进和建议.
I'm not sure if this is the best way to test the content which should be written to a file. When I test the mock_calls (call_args_list is the same) this are the arguments which are passed to the file handle. Any advice, improvements and suggestions are welcome.
解决方案
TL;DR
您的问题的核心是您应该还模拟 json.dump
以便能够正确测试将要写入文件的数据.在对您的测试方法进行一些重要调整之前,我实际上很难运行您的代码.
TL;DR
The heart of your problem is that you should be also mocking json.dump
to be able to properly test the data that is going to be written to your file. I actually had a hard time running your code until a few important adjustments were made to your test method.
- 使用
builtins.open
而不是mymmodule.open
进行模拟 - 你在一个上下文管理器中,所以你应该检查
m.return_value.__enter__.write
,但是你实际上是从 json.dump 调用 write 的,这是调用 write 的地方.(以下有关建议解决方案的详细信息) - 您还应该模拟
json.dump
以简单地验证它是用您的数据调用的
- Mock with
builtins.open
and notmymmodule.open
- You are in a context manager, so you should be checking
m.return_value.__enter__.write
, however you are actually calling the write from json.dump which is where the write will be called. (Details below on a suggested solution) - You should also mock
json.dump
to simply validate it is called with your data
简而言之,有了上面提到的问题,方法可以重写为:
In short, with the issues mentioned above, the method can be re-written as:
以下所有内容的详细信息
def test_save_data_to_file(self):
with patch('builtins.open', new_callable=mock_open()) as m:
with patch('json.dump') as m_json:
self.mc.save_data_to_file(self.data)
# simple assertion that your open was called
m.assert_called_with('/tmp/data.json', 'w')
# assert that you called m_json with your data
m_json.assert_called_with(self.data, m.return_value)
详细说明
要专注于我在您的代码中看到的问题,我强烈建议您做的第一件事,因为 open
是内置的,是从内置模拟,此外,您可以为自己节省一行通过使用 new_callable
和 as
来编写代码,所以你可以简单地这样做:
Detailed Explanation
To focus on the problems I see in your code, the first thing I strongly suggest doing, since open
is a builtin, is to mock from builtins, furthermore, you can save yourself a line of code by making use of new_callable
and as
, so you can simply do this:
with patch('builtins.open', new_callable=mock_open()) as m:
我在您的代码中看到的下一个问题是我在运行此代码时遇到了问题,直到您开始循环调用时我实际进行了以下调整:
The next problem that I see with your code as I had trouble running this until I actually made the following adjustment when you started looping over your calls:
m.return_value.__enter__.return_value.write.mock_calls
要剖析它,您必须记住的是您的方法使用的是上下文管理器.在使用上下文管理器时,您的写入工作实际上将在您的 __enter__
方法中完成.所以,从你的m
的return_value
中,你想得到__enter__
的return_value.
To dissect that, what you have to keep in mind is that your method is using a context manager. In using a context manager, the work of your write will actually be done inside your __enter__
method. So, from the return_value
of your m
, you want to then get the return_value of __enter__
.
但是,这将我们带到了您要测试的问题的核心.由于 json.dump
在写入文件时的工作方式,您在检查代码后写入的 mock_calls
实际上如下所示:
However, this brings us to the heart of the problem with what you are trying to test. Because of how the json.dump
works when writing to the file, your mock_calls
for your write after inspecting the code, will actually look like this:
<MagicMock name='open().write' id='4348414496'>
call('[')
call('{')
call('"name"')
call(': ')
call('"peter"')
call(', ')
call('"id"')
call(': ')
call('5414470')
call('}')
call(', ')
call('{')
call('"name"')
call(': ')
call('"tom"')
call(', ')
call('"id"')
call(': ')
call('5414472')
call('}')
call(', ')
call('{')
call('"name"')
call(': ')
call('"pit"')
call(', ')
call('"id"')
call(': ')
call('5414232')
call('}')
call(']')
call.__str__()
测试起来不会很有趣.因此,这将我们带到您可以尝试的下一个解决方案;模拟 json.dump
.
That is not going to be fun to test. So, this brings us to the next solution you can try out; Mock json.dump
.
你不应该测试 json.dump,你应该测试用正确的参数调用它.话虽如此,您可以按照类似的方式进行模拟并执行以下操作:
You shouldn't be testing json.dump, you should be testing calling it with the right parameters. With that being said, you can follow similar fashion with your mocking and do something like this:
with patch('json.dump') as m_json:
现在,有了它,您可以显着简化您的测试代码,以简单地验证该方法是否被您正在测试的数据调用.所以,有了它,当你把它们放在一起时,你会得到这样的东西:
Now, with that, you can significantly simplify your test code, to simply validate that the method gets called with your data that you are testing with. So, with that, when you put it all together, you will have something like this:
def test_save_data_to_file(self):
with patch('builtins.open', new_callable=mock_open()) as m:
with patch('json.dump') as m_json:
self.mc.save_data_to_file(self.data)
# simple assertion that your open was called
m.assert_called_with('/tmp/data.json', 'w')
# assert that you called m_json with your data
m_json.assert_called_with(self.data, m.return_value.__enter__.return_value)
如果您有兴趣进一步重构以使您的测试方法更简洁,您还可以将补丁设置为装饰器,让您的代码更简洁:
If you're interested in further refactoring to make your test method a bit cleaner, you could also set up your patching as a decorator, leaving your code cleaner inside the method:
@patch('json.dump')
@patch('builtins.open', new_callable=mock_open())
def test_save_data_to_file(self, m, m_json):
self.mc.save_data_to_file(self.data)
# simple assertion that your open was called
m.assert_called_with('/tmp/data.json', 'w')
# assert that you called m_json with your data
m_json.assert_called_with(self.data, m.return_value.__enter__.return_value)
检查是您最好的朋友,查看在哪些步骤调用了哪些方法,以进一步帮助测试.祝你好运.
Inspecting is your best friend here, to see what methods are being called at what steps, to further help with the testing. Good luck.
相关文章