最近出现了行业级的 Python 测试框架,这意味着 Python 测试可以编写得更简洁、更统一,能够产生更好的结果报告。本文讨论三种最流行的测试框架如何识别和收集测试,以及它们如何支持编写完整的测试层,共享共同的 setup 和 teardown 代码。
这三篇系列文章 的 第一篇文章 讨论了标准测试框架(比如 zope.testing、py.test 和 nose)给 Python 测试领域带来的革命性影响。这些框架支持更简单的测试方法,让项目不再需要为运行测试编写和维护专门的代码。第二篇文章 讨论了这些自动化解决方案如何搜索 Python 包以识别可能包含测试的模块。
本文讨论下一步,介绍框架在找到测试模块之后如何发现其中的待测试项。还讨论一些细节,比如这三种框架对共同的 setup 和 teardown 代码的支持情况。
Zope 框架中的测试发现
决定了感兴趣的模块列表之后,如何发现其中的实际测试呢?
对于 zope.testing 框架,您会发现 Zope 社区有一些有意思的现象。Zope 社区并不为解决每个问题构建大型工具,而是构建小型的功能有限的工具,这些工具能够连接在一起。目前,zope.testing 模块本身实际上根本没有提供检测测试的机制!
相反,zope.testing 让程序员自己寻找每个模块中需要运行的测试并把它们集中在一个列表中。它在每个测试模块中只寻找一个东西:test_suite() 函数,这个函数应该返回标准 unittest.TestSuite 类的实例,其中包含模块定义的测试。
使用 zope.testing 的一些程序员在 test_suite() 函数中手工地创建和维护测试列表。其他程序员通过编写定制代码发现已经定义的可用测试。但是,最有意思的方法是使用另一个 Zope 包 z3c.testsetup,它能够像其他现代 Python 测试框架一样自动地发现包中的测试。
这一现象再次说明 Zope 程序员倾向于编写小型代码块,然后使用它们构建框架,而不是编写大型的全面解决方案。z3c.testsetup 包不包含可以选择测试的命令行界面,也不包含可以显示测试结果的输出模块;它完全依靠 zope.testing 实现这些功能。
实际上,z3c.testsetup 用户一般不使用 zope.testing 的测试模块发现功能。相反,他们绕开 zope.testing 的算法,按照它的默认行为只寻找名为 test.py 的模块,然后在整个源代码树中只提供一个采用此名称的模块。在最简单的情况下,他们的 test.py 像下面这样:
import z3c.testsetup test_suite = z3c.testsetup.register_all_tests(my_package) |
这完全不通过 zope.testing 执行测试发现任务,而是依靠 z3c.testsetup 本身提供的更强大的发现机制。
可以向 register_all_tests() 函数提供几个配置选项。详细信息请参见 z3c.testsetup 文档,这里只需要介绍它的基本行为。与本文讨论的其他框架不同,z3c.testsetup 在默认情况下不关心包中每个 Python 模块的名称,而是关注它的内容。它检查所有模块以及包中的所有 .txt 或 .rst 文件,选择文本中指定了 :Test-Layer: 的文件。然后,它组合模块中的所有 TestCase 和文本文件中的所有 doctest 部分,形成测试套件。
使用 :Test-Layer: 字符串标出包含测试的文件是一种有意思的机制。它的缺点是,在浏览包的文件时,为了找到测试的位置,新程序员必须打开每个文件,至少要用 grep 命令寻找 :Test-Layer: 字符串。(更不用提 z3c.testsetup 显然必须做同样的事;这使它比那些只操作文件名的框架要慢)。
最后注意,Zope 测试框架只支持 UnitTest 实例或 doctest。正如本系列的第一篇文章中讨论的,更现代的 Python 测试框架还支持一般的 Python 函数作为有效测试。这需要不同的测试检测算法,在下面讨论的框架中就会看到。
py.test 和 nose 中的测试发现
正如前一篇文章中提到的,py.test 和 nose 框架使用相似但略有差异的规则集搜索 Python 包,寻找它们认为包含测试的模块。但是,之后它们都会遇到相同的情况:它们必须检查模块列表,寻找开发人员希望作为测试运行的函数和类。
正如在前一篇文章中看到的,py.test 往往选择单一标准,期望使用它的所有项目都遵守这一标准;而 nose 允许更丰富的定制,但是这会牺牲行为的可预测性。对于测试发现,也是如此:py.test 按照固定、不可变且可预测的规则检测测试模块中的测试,而 nose 采用灵活的可定制的规则。如果项目使用 nose 执行测试,就先必须阅读项目的 setup.cfg 文件,了解 nose 是采用通常的测试检测规则,还是采用这个项目特有的规则。
下面是 py.test 使用的过程:
- 当 py.test 检查 Python 测试模块的内部时,它收集名称以 test_ 开头的每个函数和名称以 Test 开头的每个类。无论类是否继承自 unittest.TestCase,它都会收集它们。
- 测试函数直接运行,但是对于测试类,还必须搜索方法。类实例化之后,作为测试运行名称以 test_ 开头的所有方法。
- 如果测试类继承自标准的 Python unittest.TestCase 类,py.test 框架会表现出一种古怪的行为:如果类不包含 runTest() 方法,那么即使它包含几个 test_ 方法,py.test 也会抛出异常并失败。但是,如果存在 runTest() 方法,py.test 会忽略它;这个方法必须存在,py.test 才能接受这个类,但是不会运行这个方法,因为它的名称不是以 test_ 开头的。
为了纠正这种行为,可以在项目的 conttest.py 文件中或使用 -p 命令行选项激活框架的 unittest 插件:
$ py.test -p unittest
这会让 py.test 对其行为做三个更改。首先,不再只检测名称以 Test 开头的类,还会检测继承自 unittest.TestCase 的其他类。第二,对于没有提供 runTest() 方法的 TestCase 子类,py.test 不再报告异常。第三,在类包含的测试之前和之后,以标准方式正确地运行 TestCase 子类中的所有 setUp() 和 tearDown() 方法。
尽管 nose 提供更强的定制能力,但是比较简单的测试发现过程如下:
- 当 nose 检查 Python 测试模块的内部时,它采用在选择测试模块时使用的正则表达式,收集与这个正则表达式匹配的函数和类。(在默认情况下,寻找包含单词 Test 或 test 的名称,但是可以通过命令行或配置文件提供不同的正则表达式)。
- 当 nose 检查测试类的内部时,它运行与同一正则表达式匹配的方法。
- 无须特别指定,nose 总会检测 unittest.TestCase 的子类并作为测试使用它们。但是,它会根据自己的正则表达式决定哪些方法是测试,而不使用标准的 unittest 模式 ^test。
衍生测试(Generative tests)
正如在第一篇文章中看到的,py.test 和 nose 都支持编写为简单函数的测试,这会大大简化 Python 测试的编写:
# test_new.py - simple tests functions def testTrue(self): assert True == 1 def testFalse(self): assert False == 0 |
如果只需要在某一特定环境中检查组件行为,那么测试函数和更传统的测试类都可以满足需要。但是,如果要执行一系列测试,它们基本相同,只有一些参数有差异,那么怎么办?
为了更容易实现这种测试,避免多次复制并粘贴测试函数以及设置惟一的名称,py.test 和 nose 都支持衍生测试(generative test)。这里的思想是,您提供一个测试函数,它实际上是一个迭代器,然后使用它的 yield 语句并提供调用参数,从而返回一系列函数。例如,如果希望针对一系列 Web 浏览器运行一个测试,可以编写下面这样的代码:
# test_browser.py def check(browser, page): t = TestBrowser(browser) t.load_page(page) t.check_status(200) def test_browsers(): for b in 'ie6', 'ie7', 'firefox', 'safari': for p in 'index.html', 'about.html': yield check, b, p |
对于衍生测试,py.test 更方便。因此您能够更加轻松的分辨出正在运行的测试,并在一个或多个测试失败时理解测试报告,在每个元组中提供的第一项可以是一个名称,它会作为测试名称的组成部分输出:
# Alternate yield statement, for py.test ... yield 'Page %s browser %s' % (b,p), check, b, p |
当前,许多项目使用手工测试或者只能使用 unittest 支持的功能,这些技术非常笨拙。衍生测试提供了好得多的参数化测试解决方案。
setup 和 teardown
在设计和编写测试套件时,一个大问题是如何处理共同的 setup 和 teardown 代码。许多真实的测试并不像本文给出的示例函数这么简单;它们必须执行一些复杂的操作,比如在 Firefox 中打开网页并单击 “Continue” 按钮,然后检查结果。在开始实际测试(比如打开页面并单击按钮)之前,测试必须先完成一些步骤。
现在,考虑如果一百个功能性测试都要这样执行测试,会怎么样。它们都需要通过调用共同的 setup 例程运行 Firefox,然后才能执行自己的测试。与此相应,为了取消 setup 所做的操作,可能还有 teardown 代码。这样,在测试套件中就会增加两百个额外的函数调用。每个测试函数会像下面这样: