我第一次使用Python创建了一个库,我试图利用这个项目中的机会来学习单元测试。我已经编写了第一种方法,我想为它编写一些单元测试。 (是的,我知道TDD要求我先写测试,我真的会去那里。)
该方法相当简单,但它希望该类具有file
属性集,该属性指向现有文件,并且该文件是某种类型的存档(目前仅使用zip文件,tar,rar等,稍后再加上)。该方法应该返回存档中的文件数。
我在我的项目中创建了一个名为files
的文件夹,其中包含一些示例文件,我已经手动测试了该方法,但到目前为止它仍然有效。手动测试如下所示,位于archive_file.py
文件中:
if __name__ == '__main__':
archive = ArchiveFile()
script_path = path.realpath(__file__)
parent_dir = path.abspath(path.join(script_path, os.pardir))
targ_dir = path.join(parent_dir, 'files')
targ_file = path.join(targ_dir, 'test.zip' )
archive.file = targ_file
print(archive.file_count())
我所做的就是确保在test.zip
的内容中我所期待的内容是什么。
这是file_count
的样子:
def file_count(self):
"""Return the number of files in the archive."""
if self.file == None:
return -1
with ZipFile(self.file) as zip:
members = zip.namelist()
# Remove folder members if there are any.
pruned = [item for item in members if not item.endswith('/')]
return len(pruned)
由于某些原因,我直接将其转换为单元测试似乎是错误的,其中一些原因可能无效。我依靠与当前脚本文件相关的测试文件的精确位置,我需要大量手动创建的存档文件样本,以确保我能够测试足够的变体,并且,当然,我手动将返回的值与我期望的值进行比较,因为我知道测试档案中有多少文件。
似乎对我来说,这应该尽可能自动化,但似乎这样做会非常复杂。
为这种类方法创建单元测试的正确方法是什么?
答案 0 :(得分:2)
有很多不同的方法可以解决这个问题。我喜欢思考什么是有价值的测试,从我的头脑中,我可以想到几件事:
if self.file == None
)此测试可以在两个级别进行:
单元测试逻辑
对归档对象的逻辑进行单元测试应该是微不足道的。在你的file_count方法中看起来有一些可能有价值的测试:
test_no_file_returns_negative_one
(错误条件为"希望"不经常执行代码路径,并且非常适合测试。特别是如果您的客户希望此-1
返回值。
test_zip_file_pruned_logic
这看起来是代码中非常重要的功能,如果实施不当,它会完全抛弃您的代码声称能够返回的计数
test_happy_path_file_count_successful
我喜欢使用一个单元测试来运行整个函数,使用模拟依赖项ZipFile
来确保所有内容都被覆盖,而不必运行集成测试。
测试集成
我认为对每种受支持的存档类型进行测试非常有价值。这些可能是存储在您的仓库中的静态装置,您的测试已经知道每个存档有多少文件并且会在其上断言。
我认为您的所有问题都是有效的,所有这些问题都可以通过可维护的方式得到解决和测试:
我依靠与当前脚本文件相关的测试文件的准确位置
这可以通过将您的文件夹具存储在测试包的子目录中,然后使用python获取测试包的文件路径的约定来解决:
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'fixtures')
对于可移植代码,动态生成这些路径非常重要。
我需要大量手动创建的存档文件样本,以确保我测试的变化足够
是的,有多少足够好?每个支持的归档类型至少测试一次。 (netflix必须针对他们拥有应用程序的每一台设备进行测试:),许多公司必须针对大型移动设备矩阵运行测试)我认为这里的测试覆盖率至关重要,但是尝试将所有边缘情况放到需要在单元测试中涵盖。
我手动将返回的值与我期望的值进行比较,因为我知道测试档案中有多少文件。
存档必须变为静态,您的测试将存储该信息。
要记住的一件事是测试的范围。进行练习ZipFile
的测试不会非常有价值,因为它在stdlibrary中并且已经有测试。另外,测试你的代码是否适用于所有python文件系统/ os'可能也不会很有价值,因为python已经有了这些检查。
但是,确定您的测试验证您的应用程序是否适用于它所支持的所有文件类型,我相信,这非常有价值,因为它是您和您的客户之间的合同,并说"嘿,这可以工作,让我告诉你"。与python的测试相同的方式是它和你之间的合约说'嘿;我们支持OSX / LINUX /不管让我告诉你"
答案 1 :(得分:0)
建议将修剪逻辑重构为不依赖于文件或ZipFile的单独方法。这样:
def file_count(self):
...
with ZipFile(self.file) as zip:
members = zip.namelist()
# Remove folder members if there are any.
pruned = [item for item in members if not item.endswith('/')]
return len(pruned)
变为:
def file_count(self):
...
with ZipFile(self.file) as zip:
return len(self.prune_dirs(zip.namelist()))
def prune_dirs(self, members):
# Remove folder members if there are any.
pruned = [item for item in members if not item.endswith('/')]
return pruned
现在,测试prune_dirs可以在没有任何测试文件的情况下完成。
members = ["a/", "a/b/", "a/b/c", "a/b/d"]
print archive.prune_dirs(members)
如果你想避免集成测试,那么你必须伪造或模拟ZipFile。在这种情况下,任何提供方法namelist()的对象都可以。
class FakeZipFile():
def __init__(self, filename):
self.filename = filename
def namelist(self):
return ['a', 'b/', 'c']
现在我在ArchiveFile上引入一个新方法get_helper()
class ArchiveFile():
def get_helper(self):
return ZipFile(self.filename)
def file_count(self):
...
helper = self.get_helper()
return len(self.prune_dirs(helper.namelist()))
...并覆盖Testing子类中的get_helper()。
class ArchiveFileTesting(ArchiveFile):
def get_helper(self):
return FakeZipFile(self.file);
测试类允许您覆盖ArchiveFile所需的内容,以消除对ZipFile的依赖。在您的测试中,使用测试类,我认为您有良好的覆盖率。
if __name__ == '__main__':
archive = ArchiveFileTesting()
您可能想要考虑更改名单的方法,以便您可以测试比此处显示的案例更多的案例。
答案 2 :(得分:0)
Oasiscircle和dm03514对此非常有帮助,并最终引导我找到正确的答案,尤其是dm对followup question的回答。
需要做的是使用mock
库创建一个虚假版本的ZipFile
,它不会反对实际上不是文件,而是返回有效列表使用nameslist
方法时。
@unittest.mock.patch('comicfile.ZipFile')
def test_page_count(self, mock_zip_file):
comic_file = ComicFile()
members = ['dir/', 'dir/file1', 'dir/file2']
mock_zip_file.return_value.__enter__.return_value.namelist.return_value \
= members
self.assertEqual(2, self.comic_file.page_count())
上面的__enter__.return_value
部分是必要的,因为在测试的代码中,正在上下文管理器中创建ZipFile
实例。
答案 3 :(得分:0)
模拟工作 另一种方法是建立一个环境。
对于您的用例,设置环境意味着创建一个临时目录,复制您希望在那里居住的任何类型的文件,并在其中运行测试。
您必须添加一个参数或全局,告诉您的代码在哪个目录中查找。
这对我来说效果很好。但是,我的用例有些不同,因为我正在为使用外部程序的代码编写测试,所以我无法模拟任何东西。