【问题标题】:How do I mock an open used in a with statement (using the Mock framework in Python)?如何模拟在 with 语句中使用的 open (使用 Python 中的 Mock 框架)?
【发布时间】:2010-11-20 08:47:47
【问题描述】:

如何使用unittest.mock 测试以下代码:

def testme(filepath):
    with open(filepath) as f:
        return f.read()

【问题讨论】:

    标签: python mocking with-statement


    【解决方案1】:

    Python 3

    修补builtins.open 并使用mock_open,它是mock 框架的一部分。 patch 用作 context manager 返回用于替换修补的对象:

    from unittest.mock import patch, mock_open
    with patch("builtins.open", mock_open(read_data="data")) as mock_file:
        assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")
    

    如果你想使用patch 作为装饰器,使用mock_open() 的结果作为patchnew= 参数可能有点奇怪。相反,使用patchnew_callable= 参数并记住patch 不使用的每个额外参数都将传递给new_callable 函数,如patch documentation 中所述:

    patch() 采用任意关键字参数。这些将在构造时传递给Mock(或new_callable)。

    @patch("builtins.open", new_callable=mock_open, read_data="data")
    def test_patch(mock_file):
        assert open("path/to/open").read() == "data"
        mock_file.assert_called_with("path/to/open")
    

    请记住,在这种情况下,patch 会将模拟对象作为参数传递给您的测试函数。

    Python 2

    你需要修补__builtin__.open而不是builtins.open并且mock不是unittest的一部分,你需要pip install并单独导入:

    from mock import patch, mock_open
    with patch("__builtin__.open", mock_open(read_data="data")) as mock_file:
        assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")
    

    【讨论】:

    • 感恩!我的问题有点复杂(我必须将mock_openreturn_value 引导到另一个模拟对象并断言第二个模拟对象的return_value),但它通过将mock_open 添加为new_callable 来工作。
    • 我应该如何编码我的测试以同时在 py2 和 py3 上工作?我不喜欢在测试期间检查sys.version_info 的想法
    • @ArthurZopellaro 查看six 模块,以获得一致的mock 模块。但我不知道它是否也映射到公共模块中的builtins
    • 如何找到要修补的正确名称? IE。你如何找到任意函数的@patch(在这种情况下为'builtins.open')的第一个参数?
    • 请注意,您可能需要完善您的断言。我得到AssertionError. Expected: open('file_path') Actual: open('file_path', 'r', -1, None, None)
    【解决方案2】:

    带有断言的简单@patch

    如果你想使用@patch。 open() 在处理程序内部被调用并被读取。

        @patch("builtins.open", new_callable=mock_open, read_data="data")
        def test_lambda_handler(self, mock_open_file):
            
            lambda_handler(event, {})
    

    【讨论】:

      【解决方案3】:

      源自 github sn-p,用于修补 python 中的读写功能。

      来源链接已结束here

      import configparser
      import pytest
      
      simpleconfig = """[section]\nkey = value\n\n"""
      
      def test_monkeypatch_open_read(mockopen):
          filename = 'somefile.txt'
          mockopen.write(filename, simpleconfig)
       
          parser = configparser.ConfigParser()
          parser.read(filename)
          assert parser.sections() == ['section']
       
      def test_monkeypatch_open_write(mockopen):
          parser = configparser.ConfigParser()
          parser.add_section('section')
          parser.set('section', 'key', 'value')
       
          filename = 'somefile.txt'
          parser.write(open(filename, 'wb'))
          assert mockopen.read(filename) == simpleconfig
      

      【讨论】:

        【解决方案4】:

        如果你不再需要任何文件,你可以装饰测试方法:

        @patch('builtins.open', mock_open(read_data="data"))
        def test_testme():
            result = testeme()
            assert result == "data"
        

        【讨论】:

          【解决方案5】:

          用 unittest 修补内置的 open() 函数:

          这适用于读取 json 配置的补丁。

          class ObjectUnderTest:
              def __init__(self, filename: str):
                  with open(filename, 'r') as f:
                      dict_content = json.load(f)
          

          mocked对象是open()函数返回的io.TextIOWrapper对象

          @patch("<src.where.object.is.used>.open",
                  return_value=io.TextIOWrapper(io.BufferedReader(io.BytesIO(b'{"test_key": "test_value"}'))))
              def test_object_function_under_test(self, mocker):
          

          【讨论】:

            【解决方案6】:

            最佳答案很有用,但我对其进行了扩展。

            如果您想根据传递给open() 的参数设置文件对象的值(as f 中的f),这是一种方法:

            def save_arg_return_data(*args, **kwargs):
                mm = MagicMock(spec=file)
                mm.__enter__.return_value = do_something_with_data(*args, **kwargs)
                return mm
            m = MagicMock()
            m.side_effect = save_arg_return_array_of_data
            
            # if your open() call is in the file mymodule.animals 
            # use mymodule.animals as name_of_called_file
            open_name = '%s.open' % name_of_called_file
            
            with patch(open_name, m, create=True):
                #do testing here
            

            基本上,open() 将返回一个对象,with 将在该对象上调用 __enter__()

            要正确模拟,我们必须模拟open() 以返回一个模拟对象。然后,该模拟对象应该模拟对它的 __enter__() 调用(MagicMock 将为我们执行此操作)以返回我们想要的模拟数据/文件对象(因此 mm.__enter__.return_value)。通过上述方式使用 2 个模拟来执行此操作,我们可以捕获传递给 open() 的参数并将它们传递给我们的 do_something_with_data 方法。

            我将整个模拟文件作为字符串传递给open(),而我的do_something_with_data 看起来像这样:

            def do_something_with_data(*args, **kwargs):
                return args[0].split("\n")
            

            这会将字符串转换为列表,因此您可以像处理普通文件一样执行以下操作:

            for line in file:
                #do action
            

            【讨论】:

            • 如果被测试的代码以不同的方式处理文件,例如通过调用它的函数“readline”,你可以在函数“do_something_with_data”中返回任何你想要的具有所需属性的模拟对象。
            • 有没有办法避免触摸__enter__?它绝对看起来更像是一种 hack,而不是推荐的方式。
            • enter 是诸如 open() 之类的上下文管理器的编写方式。模拟通常会有点 hacky,因为你需要访问“私人”的东西来模拟,但是这里的输入并不是 ingerintly hacky imo
            【解决方案7】:

            我可能玩得有点晚了,但是当我在另一个模块中调用 open 而无需创建新文件时,这对我有用。

            test.py

            import unittest
            from mock import Mock, patch, mock_open
            from MyObj import MyObj
            
            class TestObj(unittest.TestCase):
                open_ = mock_open()
                with patch.object(__builtin__, "open", open_):
                    ref = MyObj()
                    ref.save("myfile.txt")
                assert open_.call_args_list == [call("myfile.txt", "wb")]
            

            MyObj.py

            class MyObj(object):
                def save(self, filename):
                    with open(filename, "wb") as f:
                        f.write("sample text")
            

            通过将__builtin__ 模块中的open 函数修补到我的mock_open(),我可以模拟写入文件而不创建文件。

            注意:如果您正在使用使用 cython 的模块,或者您的程序以任何方式依赖于 cython,则需要通过在文件顶部包含 import __builtin__ 来导入 cython's __builtin__ module。如果您使用的是 cython,您将无法模拟通用 __builtin__

            【讨论】:

            • 这种方法的一种变体对我有用,因为大部分被测代码都在其他模块中,如此处所示。我确实需要确保将import __builtin__ 添加到我的测试模块中。这篇文章帮助阐明了为什么这种技术能像它一样有效:ichimonji10.name/blog/6
            【解决方案8】:

            mock_open 用于一个简单的文件read()(原来的mock_open sn-p already given on this page 更适合写入):

            my_text = "some text to return when read() is called on the file object"
            mocked_open_function = mock.mock_open(read_data=my_text)
            
            with mock.patch("__builtin__.open", mocked_open_function):
                with open("any_string") as f:
                    print f.read()
            

            请注意,根据 mock_open 的文档,这是专门针对 read() 的,因此不适用于 for line in f 等常见模式。

            使用 python 2.6.6 / mock 1.0.1

            【讨论】:

            • 看起来不错,但我无法让它与 for line in opened_file: 类型的代码一起使用。我尝试使用实现__iter__ 并使用它而不是my_text 的可迭代StringIO 进行试验,但没有运气。
            • @EvgeniiPuchkaryov 这特别适用于read(),因此不适用于您的for line in opened_file 案例;我已经编辑了帖子以澄清
            • @EvgeniiPuchkaryov for line in f: 支持可以通过将open() 的返回值模拟为a StringIO object instead 来实现。
            • 澄清一下,这个例子中的被测系统(SUT)是:with open("any_string") as f: print f.read()
            • 对于 Python 3+,只需将 __builtin__ 更改为 builtins: stackoverflow.com/questions/9047745/…。作为记录,builtins 是包含所有顶级函数的模块,请参阅文档:docs.python.org/3/library/builtins.html(编辑:有关更多详细信息,请参阅下面的更新答案)
            【解决方案9】:

            使用最新版本的 mock,您可以使用真正有用的 mock_open 助手:

            mock_open(mock=None, read_data=None)

            一个辅助函数来创建一个 使用mock来代替open。它适用于直接调用的 open 或 用作上下文管理器。

            模拟参数是要配置的模拟对象。如果没有( 默认),然后将为您创建一个 MagicMock,使用 API 仅限于标准文件句柄上可用的方法或属性。

            read_data 是文件句柄的读取方法的字符串 返回。默认为空字符串。

            >>> from mock import mock_open, patch
            >>> m = mock_open()
            >>> with patch('{}.open'.format(__name__), m, create=True):
            ...    with open('foo', 'w') as h:
            ...        h.write('some stuff')
            
            >>> m.assert_called_once_with('foo', 'w')
            >>> handle = m()
            >>> handle.write.assert_called_once_with('some stuff')
            

            【讨论】:

            • 如何检查是否有多个.write 调用?
            • @naxa 一种方法是将每个预期参数传递给handle.write.assert_any_call()。如果订单很重要,您也可以使用handle.write.call_args_list 接听每个电话。
            • m.return_value.write.assert_called_once_with('some stuff') 更好。避免注册呼叫。
            • 手动断言Mock.call_args_list 比调用任何Mock.assert_xxx 方法更安全。如果你拼错了后者,作为 Mock 的属性,它们总是会默默地通过。
            【解决方案10】:

            在 mock 0.7.0 中实现这一点的方式发生了变化,最终支持模拟 python 协议方法(魔术方法),特别是使用 MagicMock:

            http://www.voidspace.org.uk/python/mock/magicmock.html

            作为上下文管理器模拟打开的示例(来自模拟文档中的示例页面):

            >>> open_name = '%s.open' % __name__
            >>> with patch(open_name, create=True) as mock_open:
            ...     mock_open.return_value = MagicMock(spec=file)
            ...
            ...     with open('/some/path', 'w') as f:
            ...         f.write('something')
            ...
            <mock.Mock object at 0x...>
            >>> file_handle = mock_open.return_value.__enter__.return_value
            >>> file_handle.write.assert_called_with('something')
            

            【讨论】:

            • “后一种方法”展示了如何不使用使用 MagicMock(即它只是 Mock 如何支持魔术方法的一个示例)。如果您使用 MagicMock(如上),则为您预先配置了 enterexit
            • 你可以指向你的blog post,在那里你可以更详细地解释为什么/如何工作
            • 在 Python 3 中,'file' 没有定义(在 MagicMock 规范中使用),所以我改用 io.IOBase。
            • 注意:在 Python3 中内置的 file 不见了!
            • 我很困惑。为什么mock_open 在外部范围内仍然可用?
            猜你喜欢
            • 1970-01-01
            • 2011-02-26
            • 2012-03-06
            • 1970-01-01
            • 2012-04-19
            • 2014-04-11
            • 2022-07-19
            相关资源
            最近更新 更多