如何使用python unittest对写入文件的函数进行单元测试


81

我有一个Python函数,可将输出文件写入磁盘。

我想使用Python unittest模块为其编写单元测试。

我应该如何断言文件的相等性?如果文件内容与预期的一个+差异列表不同,我想得到一个错误。与unix diff命令的输出相同。

有没有官方/推荐的方法?

Answers:


48

最简单的方法是编写输出文件,然后读取其内容,读取gold(预期)文件的内容,然后将它们与简单的字符串相等性进行比较。如果它们相同,请删除输出文件。如果它们不同,则提出一个断言。

这样,当测试完成时,每个失败的测试都将用一个输出文件表示,并且您可以使用第3方工具来将它们与黄金文件进行比较(“超越比较”非常出色)。

如果您确实想提供自己的diff输出,请记住Python stdlib具有difflib模块。Python 3.1中新的unittest支持包括一个assertMultiLineEqual使用它显示差异的方法,类似于:

    def assertMultiLineEqual(self, first, second, msg=None):
        """Assert that two multi-line strings are equal.

        If they aren't, show a nice diff.

        """
        self.assertTrue(isinstance(first, str),
                'First argument is not a string')
        self.assertTrue(isinstance(second, str),
                'Second argument is not a string')

        if first != second:
            message = ''.join(difflib.ndiff(first.splitlines(True),
                                                second.splitlines(True)))
            if msg:
                message += " : " + msg
            self.fail("Multi-line strings are unequal:\n" + message)

70

我更喜欢让输出函数显式地接受文件句柄(或类似文件的对象),而不是接受文件并自己打开文件。这样,我可以将一个StringIO对象传递给我的单元测试中的输出函数,然后.read()将内容从该StringIO对象中返回(在.seek(0)调用之后),并与我的预期输出进行比较。

例如,我们将这样转换代码

##File:lamb.py
import sys


def write_lamb(outfile_path):
    with open(outfile_path, 'w') as outfile:
        outfile.write("Mary had a little lamb.\n")


if __name__ == '__main__':
    write_lamb(sys.argv[1])



##File test_lamb.py
import unittest
import tempfile

import lamb


class LambTests(unittest.TestCase):
    def test_lamb_output(self):
        outfile_path = tempfile.mkstemp()[1]
        try:
            lamb.write_lamb(outfile_path)
            contents = open(tempfile_path).read()
        finally:
            # NOTE: To retain the tempfile if the test fails, remove
            # the try-finally clauses
            os.remove(outfile_path)
        self.assertEqual(result, "Mary had a little lamb.\n")

像这样编码

##File:lamb.py
import sys


def write_lamb(outfile):
    outfile.write("Mary had a little lamb.\n")


if __name__ == '__main__':
    with open(sys.argv[1], 'w') as outfile:
        write_lamb(outfile)



##File test_lamb.py
import unittest
from io import StringIO

import lamb


class LambTests(unittest.TestCase):
    def test_lamb_output(self):
        outfile = StringIO()
        # NOTE: Alternatively, for Python 2.6+, you can use
        # tempfile.SpooledTemporaryFile, e.g.,
        #outfile = tempfile.SpooledTemporaryFile(10 ** 9)
        lamb.write_lamb(outfile)
        outfile.seek(0)
        content = outfile.read()
        self.assertEqual(content, "Mary had a little lamb.\n")

这种方法的另一个好处是,例如,如果您决定不想写入文件,而是决定写入其他缓冲区,则输出函数将更加灵活,因为它将接受所有类似文件的对象。

注意,使用StringIO假设测试输出的内容可以放入主存储器中。对于非常大的输出,可以使用临时文件方法(例如tempfile.SpooledTemporaryFile)。


2
最好将文件写入磁盘。如果您正在运行大量的单元测试,则磁盘上的IO会引起各种问题,尤其是尝试清理它们。我进行了写入磁盘的测试,tearDown删除了写入的文件。测试一次可以正常运行,然后在“全部运行”时失败。至少在Win机器上使用Visual Studio和PyTools。还有,速度。
2015年

1
虽然这是一个很好的解决方案,以测试不同的功能,它测试实际的接口,你的程序提供(如CLI工具)。当仍然是麻烦
朱斯特

1
我收到错误消息:TypeError:预期使用unicode参数,得到了“ str”
cn123h

我之所以来到这里,是因为我正在尝试编写用于逐个读取和读取分区的实木复合地板数据集的单元测试。这需要解析文件路径以获得键/值对,以将分区的适当值分配给(最终)结果熊猫DataFrame。写入缓冲区虽然不错,但不能让我解析分区值。
PMende

1
@PMende听起来您正在使用需要与实际文件系统交互的API。单元测试并不总是适合的测试级别。不要在单元测试级别上测试代码的所有部分,这是可以的。集成或系统测试也应在适当的地方使用。但是,尝试包含这些部分,并尽可能在边界之间传递简单值。参见youtube.com/watch?v=eOYal8elnZk
gotgenes,

20
import filecmp

然后

self.assertTrue(filecmp.cmp(path1, path2))

2
通过默认此做了shallow比较,其仅检查文件的元数据(修改时间,大小等)。请添加shallow=False您的示例。
famzah

2
另外,结果被缓存
famzah

11

我始终尝试避免将文件写入磁盘,即使它是专用于我的测试的临时文件夹也是如此:不实际接触磁盘会使测试更快,尤其是在代码中与文件交互很多的情况下。

假设您在名为main.py:的文件中拥有此“令人惊叹”的软件:

"""
main.py
"""

def write_to_file(text):
    with open("output.txt", "w") as h:
        h.write(text)

if __name__ == "__main__":
    write_to_file("Every great dream begins with a dreamer.")

要测试该write_to_file方法,您可以在同一个文件夹中的文件中写入类似这样的内容test_main.py

"""
test_main.py
"""
from unittest.mock import patch, mock_open

import main


def test_do_stuff_with_file():
    open_mock = mock_open()
    with patch("main.open", open_mock, create=True):
        main.write_to_file("test-data")

    open_mock.assert_called_with("output.txt", "w")
    open_mock.return_value.write.assert_called_once_with("test-data")

3

您可以将内容生成与文件处理分开。这样,您可以测试内容是否正确,而不必弄乱临时文件并在以后清理它们。

如果编写一个生成器方法来产生每一行内容,则可以使用一种文件处理方法来打开文件并调用file.writelines()行顺序进行。这两个方法甚至可以在同一类上:测试代码将调用生成器,而生产代码将调用文件处理程序。

这是显示所有三种测试方法的示例。通常,您只需要选择一个即可,具体取决于类上可以测试的方法。

import os
from io import StringIO
from unittest.case import TestCase


class Foo(object):
    def save_content(self, filename):
        with open(filename, 'w') as f:
            self.write_content(f)

    def write_content(self, f):
        f.writelines(self.generate_content())

    def generate_content(self):
        for i in range(3):
            yield u"line {}\n".format(i)


class FooTest(TestCase):
    def test_generate(self):
        expected_lines = ['line 0\n', 'line 1\n', 'line 2\n']
        foo = Foo()

        lines = list(foo.generate_content())

        self.assertEqual(expected_lines, lines)

    def test_write(self):
        expected_text = u"""\
line 0
line 1
line 2
"""
        f = StringIO()
        foo = Foo()

        foo.write_content(f)

        self.assertEqual(expected_text, f.getvalue())

    def test_save(self):
        expected_text = u"""\
line 0
line 1
line 2
"""
        foo = Foo()

        filename = 'foo_test.txt'
        try:
            foo.save_content(filename)

            with open(filename, 'rU') as f:
                text = f.read()
        finally:
            os.remove(filename)

        self.assertEqual(expected_text, text)

您可以提供示例代码吗?听起来很有趣。
buhtz

1
我为这三种方法@buhtz添加了一个示例。
唐·柯比

-1

根据建议,我做了以下工作。

class MyTestCase(unittest.TestCase):
    def assertFilesEqual(self, first, second, msg=None):
        first_f = open(first)
        first_str = first_f.read()
        second_f = open(second)
        second_str = second_f.read()
        first_f.close()
        second_f.close()

        if first_str != second_str:
            first_lines = first_str.splitlines(True)
            second_lines = second_str.splitlines(True)
            delta = difflib.unified_diff(first_lines, second_lines, fromfile=first, tofile=second)
            message = ''.join(delta)

            if msg:
                message += " : " + msg

            self.fail("Multi-line strings are unequal:\n" + message)

我创建了一个子类MyTestCase,因为我有许多需要读取/写入文件的函数,因此我确实需要具有可重用的assert方法。现在在测试中,我将继承MyTestCase而不是unittest.TestCase。

你怎么看待这件事?


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.