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
,他们往往通过安装他们的测试脚本zc.recipe.testrunner
在配方buildout.cfg
文件。 但是结果在不同的项目中是相当一致的:在我遇到的每个Zope项目中,开发构建都创建了一个./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软件包的测试。
但是,可能还有最后一级的标准化! 目前,大多数Python项目都包含一个顶级setup.py
文件,其源代码支持以下命令:
# Common commands supported by setup.py files
$ python setup.py build
$ python setup.py install
setuptools
许多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
提供了更强大的命令行选项。 但是,对于只想检查软件包是否在其平台上正常工作,然后再尝试查找错误或添加新功能的新开发人员来说, test_suite
入口点是一个绝佳的便利。
自动Python模块发现
zope.testing
, py.test
和nose
的主要特征是,它们都搜索项目的源代码树,以尝试找到其所有测试,而不必将其集中列出。 但是他们发现测试的规则有些不同,在框架之间做出选择之前可能值得回顾。
测试框架采取的第一步是选择它将搜索可能包含测试的文件的目录。 请注意,所有三个框架均始于整个项目的基本目录。 如果您正在测试名为example
的软件包,则它们都将开始在包含example
的父目录中寻找测试。 但是,对于要搜索的目录,这三个框架的选择有些不同:
-
zope.testing
工具递归地进入所有Python软件包目录,这意味着它们包含__init__.py
文件(这是向Python发出信号,可以使用import
语句将其import
)。 这意味着可以安全地检查非软件包目录中的数据和代码,但是,另一方面,这意味着您编写的每个测试从理论上讲都是程序员可以在需要时使用import
语句进行的测试。 一些程序员会感到不舒服,并希望他们可以将测试放置在使普通用户看不到它们的地方。 -
py.test
命令可以进入项目中的每个目录和子目录,无论目录看起来是否像Python包。 请注意,当两个相邻目录共享相同名称的测试时,它似乎有一个错误。 例如,如果相邻的dir1/test.py
和dir2/test.py
文件都有一个名为test_example
的测试,则py.test
将运行第一个测试两次,并完全忽略第二个测试! 如果要为py.test
编写测试并将其隐藏在非软件包目录下,请注意保持其名称唯一。 -
nose
测试运行器在其他两个工具之间实现了一种中间方式:它属于每个Python软件包,但仅愿意检查目录(如果目录名称中带有单词test
。 这意味着,如果您要在目录中编写“ Keep Out”,以便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
工具是更坚硬,并且总是寻找Python模块的名字要么开始`test_
或结束与_test
。 nosetests
命令更加灵活,它使用一个正则表达式(出于好奇,它是“((?:^ | [\ b _ \ .-])[Tt] est]”),该正则表达式选择任何以单词test
或Test
,或让该单词紧随单词边界。 您可以在命令行中使用-m
选项,或者通过在项目的.noserc
文件中设置该选项来指定其他正则表达式。
哪种方法最好? 虽然某些开发人员始终喜欢灵活性,但是,如果不能将zope.testing
工具的兴趣扩展到更多模块而不只是具有tests.py
文件名的模块上,那将是一件很麻烦的zope.testing
,在这种情况下我实际上更喜欢py.test
。 所有使用py.test
项目都必须在其测试的命名方式上共享一个通用约定,从而使其他程序员更易于阅读和维护。 当使用其他任何一个框架时,读取或创建测试文件将需要两个步骤:首先,您必须查看该特定项目用作正则表达式的内容,然后才能开始有用地检查其代码。 而且,如果您一次从事多个项目,那么您可能会发现自己必须同时遵守几种不同的测试文件命名约定。
在测试套件中包括docfile和doctest
引人注目的三人字形Python提示符>>>
在一个较长的文档中已经成为一个非常明显的信号,即该文字说明了在Python提示符下应该发生的情况。 正如在本系列的第一篇文章中所看到的,这可以在充当文档的独立文本文件中发生:
Doctest for truth and falsehood
-------------------------------
The truth values in Python, named "True" and "False",
are equivalent to the Boolean numbers one and zero.
>>> True == 1
True
>>> False == 0
True
这样的插图也可以出现在源代码内部,模块,类或函数的文档字符串中:
def count_vowels(s):
"""Count the number of vowels in a string.
>>> count_vowels('aardvark')
3
>>> count_vowels('THX')
0
"""
return len( c for c in s if c in 'aeiou')
如第一个示例中所示,当这些测试在文本文件中进行时,该文件称为docfile。 当它们出现在Python源代码内部的docstring中时,如第二个示例中所示,它们称为doctests。
由于docfile和doctests是编写文档的非常普遍的方式,该文档本身就可以作为测试(并且还会在过时并变得不正确时发出信号),因此py.test
和nose
都直接支持它们。 ( zope.testing
用户必须使用标准doctest
模块中的DocTestSuite
类为每个文件手动创建Python测试用例。)
与查找测试模块的规则一样, py.test
框架具有固定的过程以支持似乎不可配置的doctest,它跨项目选择标准化,而不是在特定项目中灵活选择。
- 如果您激活它的
-p
doctest
插件,那么它将在所有Python模块的文档字符串(甚至是名称中没有单词test
模块)以及所有名称都以开头的文本文件中查找doctest
。test_
并以.txt
扩展名结尾。 - 如果激活其
-p
restdoc
插件,则不仅将测试.txt
文件中的任何doctest,而且py.test
会坚持要求您项目中的每个.txt
文件都是有效的重组文本文件,并且如果有任何它们会导致解析错误。 还可以通过其他命令行选项,要求插件检查您在文档中指定的所有URL,并继续生成每个.txt
文档文件HTML版本。
nose
支持一组非常相似的功能,但是,您可能会猜到,它提供了更多的灵活性。
- 选择
--doctest-tests
是最--doctest-tests
受干扰的选项,它只是要求nose
注意已经在检查的测试模块的文档字符串中的doctest。 -
--with-doctest
选项更具攻击性,它要求nose
查看所有正常模块,它认为不是测试模块,但包含正常代码,并查找并运行在其文档字符串中出现的任何文档测试。 - 最后,--
--doctest-extension
允许您指定自己的文件扩展名(我知道的大多数开发人员都会选择.txt
或.rst
或.doctest
)。 这要求nose
读取项目中具有给定扩展名的所有文本文件,运行并验证找到的任何文档测试。
尽管py.test
和nose
在这里设置了非常相似的功能,但我实际上更喜欢使用方法nose
。 我喜欢对我的所有重组文本文件使用非标准.rst
扩展名,以便我可以教我的文本编辑器识别它们并为它们提供特殊的语法突出显示。
nose
框架和可执行模块
应该对nose
框架提出一个警告:默认情况下,它将避免标记为可执行的Python模块。 (您可以使用chmod
+x
命令将文件标记为Linux®上的可执行命令。) nose
框架会忽略此类文件,因为设计为直接从命令行运行的模块可能会执行使它们不安全的操作。 import
。
但是,通过使用if
语句保护命令执行的实际操作,可以检查命令是否正在运行或只是简单地导入,从而使命令可以安全地导入:
#!/usr/bin/env python
# Sample Python command
if __name__ == '__main__':
print "This has been run from the command line!"
如果你把这个预防措施在你的命令中的每一个,因此,知道他们是安全的进口,那么也可以把nose
的--exe
命令行选项,它会检查反正这样的模块。
在这种情况下,我实际上更喜欢py.test
的行为:通过忽略Python模块是否可执行,它得出的规则比nose
规则更简单,并且强制执行良好的实践(例如,使用if
语句保护命令逻辑)。 但是,如果您想对可能包含数十个您不确定其代码质量的可执行模块的遗留应用程序使用测试框架进行尝试,那么“ nose
”似乎更安全。
结论
现在,本文涵盖了有关这三个Python测试框架如何检查代码库并选择他们认为包含测试的模块的所有详细信息。 通过提供基于统一约定的自动发现,三个主要测试框架中的任何一个的选择都将帮助您编写更一致的测试套件,以供机器检测和检查。 但是,Web框架接下来会做什么? 它们是什么这些模块的内部? 这就是本系列第三篇文章的主题!
翻译自: https://www.ibm.com/developerworks/aix/library/au-pythontesting2/index.html