如何在with语句中模拟open(使用Python Mock框架)?

303 投票
11 回答
233310 浏览
提问于 2025-04-15 13:39

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

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

11 个回答

86

在最新版本的mock库中,你可以使用一个非常实用的mock_open助手:

mock_open(mock=None, read_data=None)

这是一个帮助函数,用来创建一个模拟对象,替代打开文件时使用的open函数。它可以直接用于open函数,也可以作为上下文管理器使用。

mock参数是你想要配置的模拟对象。如果你不提供这个参数(默认是None),那么系统会为你创建一个MagicMock对象,这个对象的功能仅限于标准文件句柄上可用的方法或属性。

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')
429

Python 3

在Python中,你可以对builtins.open进行修改,使用mock_open,这个功能是mock框架的一部分。使用patch作为上下文管理器时,它会返回一个用来替代被修改对象的对象:

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()的结果作为new=参数传给patch可能会有点奇怪。更好的做法是使用patchnew_callable=参数,并且要记住,所有patch不使用的额外参数都会传给new_callable函数,这在patch文档中有说明:

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

在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")
158

在mock 0.7.0版本中,处理这个问题的方法发生了变化,这个版本终于支持模拟Python的协议方法(也就是魔法方法),特别是使用MagicMock这个工具:

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

下面是一个模拟打开文件作为上下文管理器的例子(来自mock文档的示例页面):

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

撰写回答