单元测试文件修改
最近我在做的程序中,一个常见的任务就是以某种方式修改文本文件。(嘿,我在用Linux,所有东西都是文件。而且我还做大规模的系统管理。)
不过,代码要修改的文件可能在我的桌面电脑上并不存在。而且如果它真的在我的桌面上,我可能也不想去修改它。
我在《Dive Into Python》里读过关于单元测试的内容,测试一个把十进制转换成罗马数字的应用时,我知道我想做什么(这是书里的例子)。测试过程非常自给自足。你不需要验证程序是否打印出正确的内容,只需要确认函数对给定输入返回了正确的输出。
但是在我的情况下,我们需要测试程序是否正确地修改了它的环境。以下是我想到的步骤:
1) 在一个标准位置创建“原始”文件,比如说 /tmp。
2) 运行修改文件的函数,并传入 /tmp 中文件的路径。
3) 验证 /tmp 中的文件是否被正确修改;根据结果进行通过/失败的单元测试。
我觉得这样做有点笨拙。(如果你还想验证备份文件是否正确创建,那就更麻烦了。)有没有人想出更好的方法呢?
6 个回答
对于后来的读者,如果你只是想测试写入文件的代码是否正常工作,这里有一个叫“fake_open”的东西。它会修改一个模块里原本的打开文件的功能,改成使用StringIO。fake_open会返回一个打开文件的字典,这样你就可以在单元测试或文档测试中查看这些文件,而不需要真正的文件系统。
def fake_open(module):
"""Patch module's `open` builtin so that it returns StringIOs instead of
creating real files, which is useful for testing. Returns a dict that maps
opened file names to StringIO objects."""
from contextlib import closing
from StringIO import StringIO
streams = {}
def fakeopen(filename,mode):
stream = StringIO()
stream.close = lambda: None
streams[filename] = stream
return closing(stream)
module.open = fakeopen
return streams
你有两个层次的测试。
过滤和修改内容。这些是“低级别”的操作,实际上不需要真正的文件输入输出。这些测试包括决策、选择等,也就是应用程序的“逻辑”。
文件系统操作。创建、复制、重命名、删除、备份。抱歉,这些都是需要真正文件系统的操作,不能随便测试。
对于这种类型的测试,我们通常会使用一个“模拟”对象。你可以设计一个叫“FileSystemOperations”的类,里面包含各种文件系统操作。你测试这个类,确保它能基本完成读取、写入、复制、重命名等操作。这其中没有复杂的逻辑,只有调用文件系统操作的方法。
然后你可以创建一个MockFileSystem,它模拟各种操作。你可以用这个模拟对象来测试其他类。
在某些情况下,你的所有文件系统操作都在os模块里。如果是这样,你可以创建一个MockOS模块,里面包含你实际使用的操作的模拟版本。
把你的MockOS模块放到PYTHONPATH
里,这样就可以隐藏真实的OS模块。
在生产环境中,你使用经过充分测试的“逻辑”类加上你的FileSystemOperations类(或者真实的OS模块)。
你提到的是一次性测试太多内容。如果你一开始就想着“我们来验证一下它是否正确修改了环境”,那你就注定要失败了。因为环境有成百上千,甚至上百万种可能的变化。
相反,你应该关注程序的各个部分(“单元”)。比如,你会有一个函数来确定需要写入的文件位置吗?这个函数的输入是什么?可能是一个环境变量,也可能是从配置文件中读取的一些值?先测试这个函数,但不要真的去修改文件系统。不要给它“真实”的值,而是给它一些容易验证的值。在你的测试的 setUp
方法中,创建一个临时目录,并在里面放入一些文件。
然后测试写文件的代码。只需要确保它写入的内容是正确的。甚至可以不写入真实的文件系统!你不需要为此创建“假”的文件对象,只需使用Python的 StringIO
模块;它们是“真实”的文件接口实现,只是你的程序不会真的写入这些地方。
最终,你还是需要测试最终的、所有东西都真正连接起来的顶层函数,这个函数会传入真实的环境变量和配置文件,把所有东西组合在一起。但一开始不需要担心这个。首先,你在为小函数编写单独测试时,会逐渐掌握一些技巧,创建测试的模拟、假对象和桩函数会变得很自然。其次,即使你还不太会测试某个函数调用,你也会对它调用的所有内容的工作情况有很高的信心。此外,你会发现,测试驱动开发会迫使你让API更加清晰和灵活。例如:测试一个调用了来自某个抽象对象的 open()
方法的内容,比测试一个调用 os.open
并传入字符串的内容要容易得多。open
方法是灵活的;它可以被模拟,也可以有不同的实现,但字符串就是字符串,os.open
不允许你捕捉到对它调用了哪些方法。
你还可以构建测试工具来简化重复的任务。例如,Twisted 提供了创建临时文件的功能,这个功能直接集成在它的测试工具中。测试工具或大型项目的测试库中,通常会有这样的功能。