自定义unittest.mock.mock_open进行迭代

lem*_*sss 24 python iteration unit-testing mocking python-mock

我应该如何自定义unittest.mock.mock_open来处理这段代码?

file: impexpdemo.py
def import_register(register_fn):
    with open(register_fn) as f:
        return [line for line in f]
Run Code Online (Sandbox Code Playgroud)

我第一次尝试尝试read_data.

class TestByteOrderMark1(unittest.TestCase):
    REGISTER_FN = 'test_dummy_path'
    TEST_TEXT = ['test text 1\n', 'test text 2\n']

    def test_byte_order_mark_absent(self):
        m = unittest.mock.mock_open(read_data=self.TEST_TEXT)
        with unittest.mock.patch('builtins.open', m):
            result = impexpdemo.import_register(self.REGISTER_FN)
            self.assertEqual(result, self.TEST_TEXT)
Run Code Online (Sandbox Code Playgroud)

这失败了,大概是因为代码不使用read,readline或readlines.unittest.mock.mock_open 的文档说:"read_data是要返回的文件句柄的read(),readline()和readlines()方法的字符串.对这些方法的调用将从read_data中获取数据,直到它耗尽为止这些方法的模拟非常简单.如果你需要更多地控制你要测试代码的数据,你需要自己定制这个模拟.默认情况下,read_data是一个空字符串."

由于文件没有给出暗示上会需要什么样定制我试图return_valueside_effect.都没有奏效.

class TestByteOrderMark2(unittest.TestCase):
    REGISTER_FN = 'test_dummy_path'
    TEST_TEXT = ['test text 1\n', 'test text 2\n']

    def test_byte_order_mark_absent(self):
        m = unittest.mock.mock_open()
        m().side_effect = self.TEST_TEXT
        with unittest.mock.patch('builtins.open', m):
            result = impexpdemo.import_register(self.REGISTER_FN)
            self.assertEqual(result, self.TEST_TEXT)
Run Code Online (Sandbox Code Playgroud)

Mar*_*ers 34

mock_open()对象确实没有实现迭代.

如果您没有将文件对象用作上下文管理器,则可以使用:

m = unittest.mock.MagicMock(name='open', spec=open)
m.return_value = iter(self.TEST_TEXT)

with unittest.mock.patch('builtins.open', m):
Run Code Online (Sandbox Code Playgroud)

现在open()返回一个迭代器,可以直接迭代,就像文件对象一样,并且它也可以使用next().但是,它不能用作上下文管理器.

您可以将此结合起来,mock_open()然后在返回值上提供a __iter____next__方法,还有额外的好处,mock_open()它还添加了用作上下文管理器的先决条件:

# Note: read_data must be a string!
m = unittest.mock.mock_open(read_data=''.join(self.TEST_TEXT))
m.return_value.__iter__ = lambda self: self
m.return_value.__next__ = lambda self: next(iter(self.readline, ''))
Run Code Online (Sandbox Code Playgroud)

这里的返回值是一个MagicMock从specced对象file对象(Python的2)或内存中的文件中的对象(Python 3中),但只有read,write__enter__方法已被灭掉.

上面没有在Python 2工作,因为一)的Python 2预计next存在,不__next__和b)next不被视为在模拟(这是正确的)的特殊方法,所以即使你重新命名__next__next在上述示例中的类型的返回值将没有next方法.在大多数情况下,使文件对象生成可迭代而不是迭代器就足够了:

# Python 2!
m = mock.mock_open(read_data=''.join(self.TEST_TEXT))
m.return_value.__iter__ = lambda self: iter(self.readline, '')
Run Code Online (Sandbox Code Playgroud)

任何使用的代码都iter(fileobj)将起作用(包括for循环).

Python跟踪器中有一个未解决的问题,旨在弥补这一差距.

  • 这是基于OP问题,也用于[`unittest.mock`示例文档](https://docs.python.org/3/library/unittest.mock-examples.html#patch-decorators); 我想专注于`mock_open()`返回值的可迭代方面.`module_under_test.open`会稍微具体一点,是的. (2认同)

max*_*zig 7

从Python 3.6开始,该unittest.mock_open方法返回的模拟文件类对象不支持迭代.此错误已于2014年报告,并于2017年开放.

因此这样的代码默默地产生零迭代:

f_open = unittest.mock.mock_open(read_data='foo\nbar\n')
f = f_open('blah')
for line in f:
  print(line)
Run Code Online (Sandbox Code Playgroud)

您可以通过向返回正确行迭代器的模拟对象添加方法来解决此限制:

def mock_open(*args, **kargs):
  f_open = unittest.mock.mock_open(*args, **kargs)
  f_open.return_value.__iter__ = lambda self : iter(self.readline, '')
  return f_open
Run Code Online (Sandbox Code Playgroud)