【问题标题】:Mock open() function used in a class method模拟类方法中使用的 open() 函数
【发布时间】:2016-02-12 13:05:05
【问题描述】:

我试图模拟在我的类的方法中使用的 open 函数。 我找到了这个线程How do I mock an open used in a with statement (using the Mock framework in Python)?,但无法解决我的问题。此外,unittest 文档显示了一个解决方案,它也没有模拟我打开的https://docs.python.org/3/library/unittest.mock-examples.html#patch-decorators

这是我的类,其中使用了open函数的方法:

#__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()

现在我找到了一些不同的解决方案。这是我的测试:

#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 相同)时,这是传递给文件句柄的参数。 欢迎任何意见、改进和建议。

【问题讨论】:

    标签: python unit-testing mocking


    【解决方案1】:

    TL;DR

    您的问题的核心是您应该还嘲笑json.dump 以便能够正确测试将要写入文件的数据。在对您的测试方法进行一些重要调整之前,我实际上很难运行您的代码。

    • 模拟 builtins.open 而不是 mymmodule.open
    • 您在上下文管理器中,因此您应该检查m.return_value.__enter__.write,但是您实际上是从 json.dump 调用写入,这是将调用写入的位置。 (以下有关建议解决方案的详细信息)
    • 您还应该模拟 json.dump 以简单地验证它是用您的数据调用的

    简而言之,有了上面提到的问题,方法可以重写为:

    以下所有内容的详细信息

    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_callableas,所以你可以这样做:

    with patch('builtins.open', new_callable=mock_open()) as m:
    

    我在您的代码中看到的下一个问题是我在运行此代码时遇到了问题,直到您开始循环调用时我实际进行了以下调整:

    m.return_value.__enter__.return_value.write.mock_calls
    

    要剖析它,您必须记住的是您的方法使用的是上下文管理器。在使用上下文管理器时,您的写入工作实际上将在您的 __enter__ 方法中完成。所以,从你的mreturn_value 中,你想得到__enter__ 的return_value。

    但是,这将我们带到了您要测试的问题的核心。由于json.dump 在写入文件时的工作方式,您在检查代码后写入的mock_calls 实际上看起来像这样:

    <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

    你不应该测试 json.dump,你应该测试用正确的参数调用它。话虽如此,您可以按照类似的方式进行模拟并执行以下操作:

    with patch('json.dump') as m_json:
    

    现在,您可以显着简化您的测试代码,以简单地验证该方法是否被您正在测试的数据调用。所以,有了它,当你把它们放在一起时,你会得到这样的东西:

    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)
    

    如果您有兴趣进一步重构以使您的测试方法更简洁一些,您还可以将补丁设置为装饰器,让您的代码更简洁:

    @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)
    

    检查是您最好的朋友,查看在哪些步骤调用了哪些方法,以进一步帮助测试。祝你好运。

    【讨论】:

    • 感谢 idjaw,我采用了您的解决方案。但是当我将 'mymodule.open' 更改为 'builtins.open' 时,我得到了一个 AssertionError: Expected call: mock('/tmp/data.json', 'w') Not called。使用“mymodule.open”它可以完美运行。我还尝试将 builtin 作为 builtins 导入某个地方,因为使用 Python 2.7,问题仍然存在,并且没有调用 mock。顺便说一句:mock_calls 的代码看起来非常像你描述的那样。这就是我在循环中构建字符串并使用“ast”从中创建列表的原因。您的解决方案确实更优雅,代码更少。
    • @surfi 很高兴这有帮助。我没有意识到你使用的是 Python 2。你真的可以试试'__builtin__.open'。我刚刚成功了。如果您有兴趣,实际上还有另一种方法可以进一步清理您的代码。您可以将模拟修补作为装饰器进行,而不是在方法中使用上下文管理器。我可以更新显示。
    • 我不确定它在写入的情况下会改变什么,但是您应该将构造函数传递给 new_callable 参数。它应该看起来像:@patch('builtins.open', new_callable=mock_open)
    • 我正在为使用 json.load 打开 json 文件的函数编写类似的测试。有没有办法运行断言来验证从模拟文件加载的 json 对象是否正确?
    • 这样一个彻底的答案。我希望你能提供一个测试读取文件功能。
    猜你喜欢
    • 2015-06-10
    • 1970-01-01
    • 2019-04-25
    • 1970-01-01
    • 1970-01-01
    • 2016-06-26
    • 1970-01-01
    • 2021-04-02
    • 2020-10-03
    相关资源
    最近更新 更多