Python 测试框架: 寻找要测试的模块

来源:developerWorks 中国 作者:developerWorks 中国
  
最近出现了行业级的 Python 测试框架,这意味着 Python 测试可以编写得更简洁、更统一,能够产生更好的结果报告。本文讨论先进的测试框架如何提供健壮的应用程序测试自动发现,以及这如何替代过去维护的集中式测试列表。

Python 编程社区非常重视单元测试和功能性测试。这种风气不但有助于确保组件和应用程序最初的质量,还促使程序员不断调整和改进代码。

本文是讨论现代 Python 测试框架的 三篇系列文章 的第二篇。本系列中的 第一篇文章 介绍了 zope.testing、py.test 和 nose,介绍它们如何影响 Python 项目编写和维护测试的方式。本文介绍如何调用这三种框架、它们如何在项目中发现测试以及如何选择并运行测试。最后一篇文章将讨论如何通过各种报告特性让测试支持更强大的技术。

Python 测试的黑暗时代

Python 项目测试曾经是非常特殊化、个人化的活动。开发人员可能先在单独的 Python 脚本中编写每组测试。然后,编写一个名为 test_all.py 或 tests.py 的脚本,这个脚本导入并运行他的所有测试。但是,无论这个过程的自动化做得多么好,这种方式仍然是特殊化的:参与项目的每个开发人员都必须知道测试脚本放在哪里以及如何调用它们。如果某个 Python 开发人员从事十几个项目,他就必须记住十几个测试命令。

test_all.py(或项目采用的其他名称)还可能手工导入所有其他测试,这可能导致风险。如果这个集中的测试列表过时了(常常是由于开发人员添加了新的测试套件,手工运行它,但是忘了把它添加到中心脚本中),那么在 Python 包投入生产之前的最后一次测试就会遗漏许多测试。

这种无政府状态的另一个缺点是,它要求每个测试文件包含样板代码,从而能够作为单独的命令运行。如果查看 Python 文档或当今的一些 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()

本系列的 第一篇文章 已经讨论过基于 TestCase 类的测试在现代环境中为什么常常是不必要的。但是,现在注意最后两行:它们起什么作用?答案是,它们检测什么时候从命令行单独运行这个 test_old.py 脚本,在这种情况下,它们运行一个 unittest 简便函数,这个函数在模块中搜索测试并运行它们。它们使这个测试文件可以独立于项目范围的测试脚本单独运行。

显然,在数十甚至数百个测试模块中复制相同的代码非常麻烦。另一个不太明显的缺点是这种做法不利于标准化。如果 test_main() 函数不够完善,无法检测出某个模块的测试,那么这个模块的行为可能与其他测试套件不匹配。因此,每个模块在测试类的名称、操作方式和运行方式方面稍有差异。





Python 测试的开放时代

由于主流 Python 测试框架的出现,上述的所有问题已经解决了,而且每种框架解决这些问题的方式大致相同。

首先,这三种测试框架都提供了从操作系统命令行运行测试的标准方法。这样,每个 Python 项目就不再需要在代码基中维护全局测试脚本。

zope.testing 包运行测试的机制是最特殊化的:因为 Zope 开发人员常常使用 buildout 设置他们的项目,常常通过 buildout.cfg 文件中的 zc.recipe.testrunner recipe 安装测试脚本。但是,结果在不同的项目上相当一致:在我遇到的每个 Zope 项目中,开发 buildout 都会创建一个 ./bin/test 脚本,可以通过它调用项目的测试。

py.test 和 nose 项目的做法更意思。它们都提供一个命令行工具,所以每个项目完全不需要有自己的测试命令:

# Run "py.test" on the project
# in the current directory...

$ py.test

# Run "nose" on the project
# in the current directory...

$ nosetests

py.test 和 nosetests 工具甚至有几个相同的命令行选项,比如 -v 选项在执行测试时输出测试的名称。可能过不了多久,只要程序员熟悉这两种工具,就能够运行大多数公共 Python 包的测试。

但是,还有最后一级标准化!当今的大多数 Python 项目在源代码中包含一个顶级 setup.py 文件,它支持下面的命令:

# Common commands supported by setup.py files

$ python setup.py build
$ python setup.py install

当今的许多 Python 项目使用 setuptools 包支持标准 Python 没有提供的 setup.py 命令,包括运行项目的所有测试的 test 命令:

# If a project's setup.py uses "setuptools"
# then it will provide a "test" command too

$ python setup.py test

这是标准化的最高层次:如果项目都以一致的方式支持 setup.py test,开发人员就可以通过统一的接口运行所有 Python 包的测试套件。nose 通过提供一个入口点支持 setup.py,这个入口点调用与 nosetests 命令相同的测试运行例程:

# A setup.py file that uses "nose" for testing

from setuptools import setup

setup(
    # ...
    # package metadata
    # ...
    setup_requires = ['nose'],
    test_suite = 'nose.collector',
    )

当然,即使项目提供了 setup.py 入口点,大多数开发人员可能仍然使用 nosetests,因为 nosetests 提供更强大的命令行选项。但是对于新的开发人员,如果只想在调试 bug 或添加新特性之前检查包是否能够在他的平台上工作,那么 test_suite 入口点是非常方便的。





自动 Python 模块发现

zope.testing、py.test 和 nose 的一个关键特性是,它们都可以搜索项目的源代码树,寻找项目的所有测试,所以不需要集中的测试列表。但是,它们采用的测试发现规则不太一样,在选择框架时需要考虑到这一点。

测试框架执行的第一步是,选择将在哪些目录中搜索包含测试的文件。注意,这三种框架都从整个项目的基目录开始搜索;如果要测试名为 example 的包,那么它们会从包含 example 的父目录开始搜索测试。但是,这三种框架在选择搜索哪些目录方面有所差异:

  • zope.testing 工具向下递归地搜索是 Python 包的所有目录,也就是包含 __init__.py 文件的目录(对于 Python,这说明可以用 import 语句导入它们)。这意味着不检查非包目录中的数据和代码,但是另一方面,这也意味着从理论上说程序员可以用 import 语句导入您编写的每个测试。一些程序员觉得这让人不舒服,希望能够把测试放在包的一般用户看不到的地方。
  • py.test 命令向下递归地搜索项目的每个目录和子目录,无论目录是否是 Python 包。注意,当两个相邻目录包含同名的测试时,它似乎有一个 bug。例如,如果相邻的 dir1/test.py 和 dir2/test.py 文件都包含名为 test_example 的测试,那么 py.test 将运行第一个测试两次,而完全忽略第二个测试!如果为 py.test 编写测试并把它们放在非包目录中,就要注意保持名称是惟一的。
  • nose 测试运行器采用的实现方式介于另两种工具之间:它向下递归地搜索每个 Python 包,但是只检查目录名中包含单词 test 的目录。这意味着,如果不想让 nose 搜索某个目录,那么只需注意不在目录名中包含 test 即可。与 py.test 不同,nose 可以正确地处理包含同名测试的相邻目录(但是保持测试名称惟一仍然是有帮助的,这样在用 -v 选项显示测试结果时不容易混淆)。

选择了要搜索的目录之后,这三种测试工具的做法就非常相似了:它们都寻找与某一模式匹配的 Python 模块(也就是以 .py 结尾的文件)。zope.testing 工具在默认情况下使用正则表达式 "tests",也就是只寻找名为 tests.py 的文件,忽略其他所有文件。可以使用命令行选项或 buildout.cfg 指定另一个正则表达式:

# Snippet of a buildout.cfg file that searches for tests
# in any Python module starting with "test" or "ftest".

[test]
recipe = zc.recipe.testrunner
eggs = my_package
defaults = ['--tests-pattern', 'f?test']

py.test 更死板,总是寻找名称以 `test_ 开头或以 _test 结尾的 Python 模块。nosetests 命令更灵活,它使用一个正则表达式(“((?:^|[\b_\.-])[Tt]est)”)选择以 test 或 Test 开头或这个单词处于单词边界后面的模块。通过在命令行上使用 -m 选项或在项目的 .noserc 文件中设置这个选项,可以指定另一个正则表达式。

哪种方法最好?尽管一些开发人员喜欢有灵活性,而且许多人认为 zope.testing 工具的搜索范围应该更宽,不应该只限于文件名为 tests.py 的模块,但是我实际上更喜欢 py.test 采用的方式。所有使用 py.test 的项目必须在测试命名方面采用一致的约定,这让其他程序员更容易阅读和维护测试。在使用另外两种框架时,阅读或创建测试文件需要两步:首先,必须了解这个项目使用的正则表达式,然后才能检查它的代码。如果您同时从事多个项目,就必须记住几种不同的测试文件命名约定。


时间:2009-07-20 13:18 来源:developerWorks 中国 作者:developerWorks 中国 原文链接

好文,顶一下
(3)
100%
文章真差,踩一下
(0)
0%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量