如何修复mock_open调用差异但结果一致的问题

5 投票
3 回答
1346 浏览
提问于 2025-04-17 20:57

使用 mock_open,我可以通过 with [...] as 这种写法来捕捉写入的数据。不过,验证我得到的数据是否正确有点棘手。例如,我可以这样做:

>>> from mock import mock_open
>>> m = mock_open()
>>> with patch('__main__.open', m, create=True):
...     with open('foo', 'w') as h:
...         h.write('some stuff')
...
>>> m.mock_calls
[call('foo', 'w'),
 call().__enter__(),
 call().write('some stuff'),
 call().__exit__(None, None, None)]
>>> m.assert_called_once_with('foo', 'w')
>>> handle = m()
>>> handle.write.assert_called_once_with('some stuff')

但我想对比一下我认为应该写入的内容和实际写入的内容。也就是说,我想做类似这样的事情:

>>> expected = 'some stuff'
>>> assert(expected == m.all_that_was_written)

我遇到的问题是,call 的不同版本的 json(2.0.9 和 1.9)似乎输出的格式不一样。并不是说我可以简单地更新到最新的 json。

我实际遇到的错误是:

E           AssertionError: [call('Tool_000.json', 'w'),
                             call().__enter__(),
                             call().write('['),
                             call().write('\n  '),
                             call().write('"1.0.0"'),
                             call().write(', \n  '),
                             call().write('"2014-02-27 08:58:02"'),
                             call().write(', \n  '),
                             call().write('"ook"'),
                             call().write('\n'),
                             call().write(']'),
                             call().__exit__(None, None, None)] 
            !=
                            [call('Tool_000.json', 'w'),
                             call().__enter__(),
                             call().write('[\n  "1.0.0"'),
                             call().write(', \n  "2014-02-27 08:58:02"'),
                             call().write(', \n  "ook"'),
                             call().write('\n'),
                             call().write(']'),
                             call().__exit__(None, None, None)]

实际上,调用的方式不同,但最终结果是一样的。

我正在测试的代码相对简单:

with open(get_new_file_name(), 'w') as fp:
    json.dump(lst, fp)

所以,创建另一个传递文件指针的方法似乎有点多余。

3 个回答

1

我将要做的是,写一个方法,这个方法会返回所有调用写入方法时的完整字符串。

class FileIOTestCase(unittest.TestCase):
    """ For testing code involving file io operations """
    def setUp(self):
        """ patches the open function with a mock, to be undone after test. """
        self.mo = mock_open()

        patcher = patch("builtins.open", self.mo)
        patcher.start()
        self.addCleanup(patcher.stop)

    def get_written_string(self):
        return ''.join(c[0][0] for c in self.mo.return_value.write.call_args_list)

下面是一个使用这个方法的例子

class TestWriteFile(FileIOTestCase):
    def test_write_file__csv(self):
        save.write_file("a,b\n1,2", "directory", "C6L")
        self.mo.assert_called_once_with(os.path.join("directory", "C6L.csv"), 'w')
        self.assertEqual(self.get_written_string(), "a,b\n1,2")
2

mock_open 这个工具还不够完善。目前它在模拟读取文件时表现不错,但在测试写入文件的内容时功能就不够强大了。这个问题很明显地显示了它的不足之处。

我的建议是,如果你要测试写入的内容,就不要使用 mock_open。下面是一个替代方案:

import six
import mock
import unittest

class GenTest(unittest.TestCase):
    def test_open_mock(self):
        io = six.BytesIO()
        io_mock = mock.MagicMock(wraps=io)
        io_mock.__enter__.return_value = io_mock
        io_mock.close = mock.Mock() # optional
        with mock.patch.object(six.moves.builtins, 'open', create=True, return_value=io_mock):
            # test using with
            with open('foo', 'w') as h:
                expected = 'some stuff'
                h.write(expected)
            self.assertEquals(expected, io.getvalue())
            # test using file handle directly
            io.seek(0); io.truncate() # reset io
            expected = 'other stuff'
            open('bar', 'w').write(expected)
            self.assertEquals(expected, io.getvalue())
            # test getvalue after close
            io.seek(0); io.truncate() # reset io
            expected = 'closing stuff'
            f = open('baz', 'w')
            f.write(expected)
            f.close()
            self.assertEquals(expected, io.getvalue())

if __name__ == '__main__':
    unittest.main()
2

你可以修改 open() 函数,让它返回一个 StringIO 对象,然后你就可以检查里面的内容了。

with mock.patch('module_under_test.open', create=True) as mock_open:
    stream = io.StringIO()
    # patching to make getvalue() work after close() or __exit__()
    stream.close = mock.Mock(return_value=None)
    mock_open.return_value = stream

    module_under_test.do_something() # this calls open()

    contents = stream.getvalue()
    assert(contents == expected)

编辑: 添加了对 stream.close 的修改,以避免在调用 stream.getvalue() 时出现异常。

撰写回答