如何用unittest.mock测试下面的代码:

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

当前回答

这样做的方式在mock 0.7.0中已经改变,它最终支持模拟python协议方法(magic方法),特别是使用MagicMock:

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

一个作为上下文管理器打开的mock示例(来自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')

其他回答

这样做的方式在mock 0.7.0中已经改变,它最终支持模拟python协议方法(magic方法),特别是使用MagicMock:

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

一个作为上下文管理器打开的mock示例(来自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')

在我的例子中,我使用的是pytest,好消息是在Python 3中,单元测试库也可以导入和使用,没有问题。

以下是我的方法。首先,我用可重用的pytest fixture创建了一个conftest.py文件:

from functools import cache
from unittest.mock import MagicMock, mock_open

import pytest
from pytest_mock import MockerFixture


class FileMock(MagicMock):

    def __init__(self, mocker: MagicMock = None, **kwargs):
        super().__init__(**kwargs)

        if mocker:
            self.__dict__ = mocker.__dict__
            # configure mock object to replace the use of open(...)
            # note: this is useful in scenarios where data is written out
            _ = mock_open(mock=self)

    @property
    def read_data(self):
        return self.side_effect

    @read_data.setter
    def read_data(self, mock_data: str):
        """set mock data to be returned when `open(...).read()` is called."""
        self.side_effect = mock_open(read_data=mock_data)

    @property
    @cache
    def write_calls(self):
        """a list of calls made to `open().write(...)`"""
        handle = self.return_value
        write: MagicMock = handle.write
        return write.call_args_list

    @property
    def write_lines(self) -> str:
        """a list of written lines (as a string)"""
        return ''.join([c[0][0] for c in self.write_calls])


@pytest.fixture
def mock_file_open(mocker: MockerFixture) -> FileMock:
    return FileMock(mocker.patch('builtins.open'))

在这里,我决定将read_data作为一个属性,以便更python化。它可以用open()需要返回的任何数据在测试函数中赋值。

在我的测试文件中,命名为test_it_works.py,我有以下测试用例来确认预期的功能:

from unittest.mock import call


def test_mock_file_open_and_read(mock_file_open):
    mock_file_open.read_data = 'hello\nworld!'

    with open('/my/file/here', 'r') as in_file:
        assert in_file.readlines() == ['hello\n', 'world!']

    mock_file_open.assert_called_with('/my/file/here', 'r')


def test_mock_file_open_and_write(mock_file_open):
    with open('/out/file/here', 'w') as f:
        f.write('hello\n')
        f.write('world!\n')
        f.write('--> testing 123 :-)')

    mock_file_open.assert_called_with('/out/file/here', 'w')

    assert call('world!\n') in mock_file_open.write_calls

    assert mock_file_open.write_lines == """\
hello
world!
--> testing 123 :-)
""".rstrip()

点击这里查看要点。

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

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

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

被模拟的对象是io。由open()函数返回的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):

上面的答案很有用,但我对它进行了一些扩展。

如果你想根据传递给open()的参数来设置文件对象的值(f in as 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__()。

为了正确地模拟,必须mock open()返回一个模拟对象。然后,该模拟对象应该模拟对其进行的__enter__()调用(MagicMock将为我们完成此操作),以返回我们想要的模拟数据/文件对象(因此是mm.__enter__.return_value)。用上述方法对2个mock执行此操作允许我们捕获传递给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

如果你不需要进一步的文件,你可以装饰测试方法:

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