Python 测试框架: 用 Python 测试框架简化测试

最近出现了行业级的 Python 测试框架,这意味着 Python 测试可以编写得更简洁、更统一,能够产生更好的结果报告。本文介绍比较先进的测试框架并讨论它们的基本特性。

Brandon Craig Rhodes, 独立顾问, Rhodes Mill Studios, Inc.

Brandon Craig Rhodes 是 Python Magazine 的主编和独立的 web 应用程序顾问,在 Python 语言方面具有十年以上的经验。他维护几大洲的天文学家所使用的 PyEphem 扩展模块已经超过九年了,这个模块为行业级计算的天文学例程提供面向对象的接口。Brandon 是 Python Atlanta 用户组的成员。



2009 年 6 月 22 日

Python 测试领域的蛮荒时代结束了。仅仅在几年前,几乎每个用 Python 构建的项目在编写和运行测试方面都采用自己的习惯做法。但是现在,这种混乱局面终于要结束了。社区中已经出现了几种出色的测试解决方案,它们为数百个流行的项目的测试套件提供约定和通用标准。

本文是 三篇系列文章 的第一篇,本系列要介绍新的测试框架。在本文中,将介绍三种流行的测试框架,讨论新一代工具鼓励的简单测试风格。第二篇文章发现和选择测试 讨论更大的问题:这些框架如何自动地发现和分类项目的测试。最后一篇文章用 Python 测试框架生成测试报告 讨论这些框架为查看测试结果提供的强大特性。

通过学习这三种框架的通用习惯做法,您不但可以更好地理解其他程序员编写的 Python 包,还可以为自己的应用程序构建优雅、强大的测试套件。

候选框架:三种 Python 测试框架

有三种 Python 测试框架是目前使用最为广泛的。下面按时间次序简要介绍它们:

  • zope.testing

    通常,从事 Zope 项目的开发人员就像是早期的拓荒者。他们需要以一种统一的方式在大型代码基上发现和运行测试,为此开发了 zope.testing 包,这个包现在仍然得到广泛使用。

    zope.testing 包只支持 unittestdoctest 等传统 Python 测试风格,而不支持更现代的框架支持的简化风格。但是,它提供一个强大的分层系统,在这种系统中包含测试的目录可以依赖于通用的设置代码,设置代码为层(而不是每个测试)创建一个运行测试所需的环境。

  • py.test

    2004 年,Holger Krekel 把他的 std 包改名为 ‘py’,因为原来的名称常常与 Python 附带的标准库混淆。尽管这个包包含另外几个子包,但是现在最著名的部分只有它的 py.test 框架。

    py.test 框架建立了 Python 测试的新标准,目前许多开发人员都采用这种标准。它为编写测试提供了优雅的符合 Python 风格的习惯做法,让开发人员能够以非常紧凑的风格编写测试套件。

  • nose

    nose 项目是于 2005 年发布的,也就是 py.test 改名后的一年。它是由 Jason Pellerin 编写的,支持与 py.test 相同的测试习惯做法,但是这个包更容易安装和维护。尽管 py.test 在某些方面有所进步,目前也很容易安装,但是 nose 仍然保持了易用性方面的声誉。

    在 Python 大会上,常常会看到开发人员穿着黑色 T 恤衫,上面印着 nosetests 命令,后面是表示测试成功的点号。有意思的是,随着 nose 的发展,在项目邮件列表上常常看到开发人员向项目负责人询问他们的项目什么时候可以转换到 nose

在这三种框架中,nose 看起来会成为标准,py.test 的用户群比较小,但是很忠诚,zope.testing 只在基于 Zope 框架的项目中受欢迎。但是,这三种框架都得到积极的维护,各有一些独特的特性。建议您了解它们的特性和差异,选择适合自己项目的框架。


测试演化

py.test 框架接受普通的 Python 函数作为测试,而不要求把测试放在更大更重型的测试类中,这开启了 Python 测试的新时代。因为 nose 框架支持相同的习惯做法,所以这些模式很可能越来越流行。

假设希望检查 Python 真假值 TrueFalse 是否真的等于布尔数字 10py.testnose 接受并运行以下代码行,作为回答此问题的有效测试:

# test_new.py - simple tests functions

def testTrue(self):
    assert True == 1

def testFalse(self):
assert False == 0

为了体会以上示例的简单性,可以对比过去的 Python 测试文档中复杂的示例测试,比如:

# test_old.py - The old way of doing things

import unittest

class TruthTest(unittest.TestCase):
    def testTrue(self):
        assert True == 1

    def testFalse(self):
        assert False == 0

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

看到了吗?这么多代码只是为了支持两行测试代码!首先,代码需要一个 import 语句,这与要测试的代码完全无关,因为测试本身简单地忽略模块,只使用内置的 Python 值,比如 TrueFalse。另外,要创建一个类,但是它不支持或增强测试,因为测试实际上没有使用它们的 self 参数做任何事情。最后,需要两行固定不变的代码,这样才能从命令行运行这个测试。

有使用 unittest 经验的用户可能认为,上面的示例应该使用 TruthTest 类从 TestCase 类继承的测试方法。例如,他们会建议使用 assertEqual(),而不是手工测试是否相等的 assert 语句,在这种情况下测试会使用 self 而不是忽略它:

# alternate version of the TestTrue method
    ...
    def testTrue(self):
        self.assertEqual(True, 1)
        ...

对这个建议有三条反对意见。

首先,调用方法会影响可读性。尽管 assertEqual() 方法名能够表明要测试两个值是否相等,但是代码看起来仍然不像是比较,对于熟悉 Python 语言的开发人员,不如 Python == 操作符那么明确。

第二,正如在本系列的第三篇文章中将看到的,新的测试框架现在知道如何检查 assert 语句,从而查明造成测试失败的条件,这意味着简单的 assert 语句现在能够产生有意义的测试失败消息,它提供的信息与调用 assertEqual() 等老方法的结果差不多。

最后,即使 assertEqual() 仍然是必要的,但是从测试模块导入这个函数(而不是通过类继承让函数可用)肯定更简单,更符合 Python 的风格。实际上,在下面会看到,当 py.testnose 要提供更多用来支持测试的例程时,它们只需把这些例程定义为函数,然后用户可以把这些函数导入自己的代码。

当然,如果作者确实需要通过例程缓存状态,以便以后在测试用例中使用,unittest 子类仍然是有意义的,py.testnose 完全支持它们。另外,目前许多 Python 测试编写为 Python 标准库支持的 doctest,它们不需要使用函数或类:

Doctest For The Above Example
-----------------------------

The truth values in Python, named "True" and "False",
are equivalent to the Boolean numbers one and zero.

>>> True == 1
True
>>> False == 0
True

但是,如果程序员希望编写简单的测试代码,不愿意考虑 doctest 涉及的乱七八糟的东西,那么测试函数是很好的方法。总之,测试函数可以极大地增强编写测试的简便性。程序员不需要记住、重写或复制以前编写的测试代码,新的约定让 Python 程序员能够像编写一般 Python 代码一样编写测试:只需打开一个空文件,然后输入!


框架特有的约定

py.testnose 框架都提供特殊的例程,这些例程可以简化测试的编写。可以认为它们分别提供一种方便的测试 “方言”,可以用这些 “方言” 编写测试。这会简化测试的编写并减少错误,还会使测试更简短、可读性更好。但是,使用这些例程还会导致一个重要的后果:您的测试与提供函数的框架捆绑在一起了,丧失了兼容性。

因此,要权衡考虑方便性和兼容性。如果只使用笨拙的标准 Python unittest 模块从头编写所有测试,那么它们能够在任何测试框架中运行。更进一步,如果采用简单的做法编写测试函数(如上所述),那么测试至少能够在 py.testnose 中运行。但是,如果开始使用某个测试框架特有的特性,那么如果以后另一个框架开发出了新的重要特性,您决定进行框架迁移,就必须重写测试。

py.testnose 都为 TestCaseassertRaises() 方法提供了替代品。py.test 提供的版本比较新颖,它也可以接受要执行的字符串,这更强大,因为可以测试引发异常的表达式,而不只是函数调用:

# conveniences.py
import math

import py.test
py.test.raises(OverflowError, math.log, 0)
py.test.raises(ValueError, math.sqrt, -1)
py.test.raises(ZeroDivisionError, "1 / 0")

import nose.tools
nose.tools.assert_raises(OverflowError, math.log, 0)
nose.tools.assert_raises(ValueError, math.sqrt, -1)
# No equivalent for third example!

但是,除了都能够测试异常之外,这两种框架的差异就比较大了。py.test 只提供一个简便函数,它用来判断特定调用是否会触发 DeprecationWarning

		    py.test.deprecated_call(my.old.function, arg1, arg2)

另一方面,nose 提供非常丰富的断言函数,适用于希望避免 assert 语句和需要进行比较复杂的测试的情况。详细信息请参考它的文档,下面列出 nose 提供的断言函数:

# nose.tools support functions for writing tests

assert_almost_equal(first, second, places=7, msg=None)
assert_almost_equals(first, second, places=7, msg=None)
assert_equal(first, second, msg=None)
assert_equals(first, second, msg=None)
assert_false(expr, msg=None)
assert_not_almost_equal(first, second, places=7, msg=None)
assert_not_almost_equals(first, second, places=7, msg=None)
assert_not_equal(first, second, msg=None)
assert_not_equals(first, second, msg=None)
assert_true(expr, msg=None)
eq_(a, b, msg=None)
ok_(expr, msg=None)

在处理浮点数时,如果希望测试能够灵活地对待 Python 实现,允许对浮点数的处理有细小的误差,那么上面检查近似值的例程尤其有意义。


分布式测试

目前,测试的运行频率越来越高了。许多团队已经采用了连续测试,也就是对团队版本控制系统的每次签入(check-in)都要运行项目测试。随着测试驱动开发方法越来越流行,许多开发人员在开始编写新模块的代码之前先为模块编写和运行测试。如果测试的运行时间很长,就会严重影响开发人员的生产力。

因此,用尽可能多的计算能力运行测试是很有益处的。对于小规模项目,这可能意味着使用计算机的所有 CPU 核运行多个测试进程。对于更大的项目,要配置完整的测试计算机群,要么使用专用服务器并行地运行测试,要么使用所有开发人员工作站的空闲时间。

在并行和分布式测试方面,本文讨论的三种测试框架有非常显著的差异:

  • zope.testing 命令行有一个 -j 选项,它指定应该启动多个测试进程,而不是在同一进程中执行所有测试。因为每个进程可以在不同的 CPU 核上运行,所以如果在有四个 CPU 的计算机上运行 -j 4,就可以同时用四个 CPU 运行测试。
  • nose 项目报告说,他们已经把对并行测试的支持提交到项目 trunk,但是一般用户要等到下一个版本才能试用这个特性。
  • py.test 工具支持一个多处理选项 (-n),可以像 zope.testing 一样在多个 CPU 核上运行测试。另外,它还提供管理工具,可以在整个测试服务器群中分布测试。

在这三种框架中,py.test 看起来在并行测试方面明显领先了。可以指定多个 --tx 选项,每个选项描述要运行测试的一个环境或远程服务器。而且,它实际上支持针对两个完全不同的原因分布测试!在使用 --dist=load 时,它将使用服务器群按照传统方式在多台计算机上运行测试,从而减少测试花费的时间。但是,在使用 dist=each 时,它的做法就比较复杂了;它确保每个测试在 py.test 可用的每个测试环境中运行。

这意味着 py.test 可以同时在多个 Python 解释器版本和多个操作系统上运行测试。因此,如果项目支持多个平台,希望测试解决方案能够自动地支持多平台,而不需要通过编写脚本把测试复制到不同的平台并运行它们,那么 py.test 的这个特性是非常吸引人的。


定制和可扩展性

这三种测试框架都为单独的用户和整个项目提供定制能力,可以根据需要选择测试框架的行为和选项。

  • 在 Zope 包中,指定默认选项的 buildout 常常调用 zope.testing 模块。这意味着运行测试的开发人员会得到统一的结果集。但是,如果在项目级上选择的行为无法满足他们的需要,他们仍然可以指定自己的命令行开关。
  • nose 框架通过用户主目录中的 nose.cfg.noserc 文件支持用户个性化定制,用户可以在这些文件中指定测试结果的显示方式。
  • 对于 py.testnose 框架,都可以提供针对整个项目的选项。py.test 框架会在它测试的项目中寻找 conftest.py 文件,然后在这个文件中寻找针对整个项目的选项,比如是否检测并运行 doctest 以及应该使用哪种模式检测测试文件和函数。nose 框架寻找项目范围的 setup.cfg 文件(这是提供 Python 包的相关信息的标准方式),然后在其中寻找 [nosetests] 节。

另外,py.testnose 都支持插件,这些用户提供的模块可以安装新的命令行选项和在工具中添加新行为,但是它们提供的配置和能够实现的功能有差异。


结束语

在过去,每个 Python 项目都使用自己的习惯做法;采用新一代 Python 测试框架,就能够提供简洁的习惯做法和统一的测试技术。下一篇文章 要开始研究每种框架实现的测试机制,也就是它们用来搜索测试模块和测试文件的技术。请继续阅读下一篇文章。

参考资料

学习

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=AIX and UNIX
ArticleID=398577
ArticleTitle=Python 测试框架: 用 Python 测试框架简化测试
publish-date=06222009