我要坦白一点。尽管我是一个应用相当广泛的公共域 Python 库的创造者,但在我的模块中引入的单元测试是非常不系统的。实际上,那些测试大部分 是包括在 gnosis.xml.pickle 的 Gnosis Utilities 中的,并由该子软件包(subpackage)的贡献者所编写。我还发现,我下载的绝大多数第三方 Python 包都缺少完备的单元测试集。
不仅如此,Gnosis Utilities 中现有的测试也受困于另一个缺陷:您经常需要在极其大量的细节中去推定期望的输出,以确定测试的成败。测试实际上 -- 在很多情况下 -- 更像是使用库的某些部分的小实用工具。这些测试(或实用工具)支持来自任意数据源(类型正确)的输入和/或描述性数据格式的输出。实际上,当您需要调试一些细微的错误时,这些测试实用工具更有用。但是对于库版本间变化的自解释的完整性检查(sanity checks)来说,这些类测试就不能胜任了。
在这一期文章中,我尝试使用 Python 标准库模块 doctest 和 unittest 来改进我的实用工具集中的测试,并带领您与我一起体验(并指出一些最好的方法)。
脚本 gnosis/xml/objectify/test/test_basic.py 给出了一个关于当前测试的缺点及解决方案的典型示例。下面是该脚本的最新版本:
清单 1. test_basic.py
"Read and print and objectified XML file"
import sys
from gnosis.xml.objectify import XML_Objectify, pyobj_printer
if len(sys.argv) > 1:
for filename in sys.argv[1:]:
for parser in ('DOM','EXPAT'):
try:
xml_obj = XML_Objectify(filename, parser=parser)
py_obj = xml_obj.make_instance()
print pyobj_printer(py_obj).encode('UTF-8')
sys.stderr.write("++ SUCCESS (using "+parser+")\n")
print "="*50
except:
sys.stderr.write("++ FAILED (using "+parser+")\n")
print "="*50
else:
print "Please specify one or more XML files to Objectify."
实用工具函数 pyobj_printer() 生成了任意 Python 对象(具体说是这样一个对象,它既没有用到 gnosis.xml.objectify 的任何其他实用工具,也没有用到 Gnosis Utilities 中的 任何其他东西)的一个 非-XML 表示。在以后的版本中,我将可能会把这个函数移到 Gnosis 包内的其他地方。无论如何, pyobj_printer() 使用各种类-Python 的缩进和符号来描述对象和它们的属性(类似于 pprint ,但是扩展了实例,而不仅限于扩展内置的数据类型)。
如果一些特别的 XML 可能不能正确被地"对象化(objectified)", test_basic.py 脚本会提供一个很好的调试工具 -- 您可以可视化地查看结果对象的属性和值。此外,如果您重定向了 STDOUT,您可以查看 STDERR 上的简单消息,如这个例子中:
清单 2. 分析 STDERR 结果消息
$ python test_basic.py testns.xml > /dev/null
++ SUCCESS (using DOM)
++ FAILED (using EXPAT)
不过,上面运行的例子中对成功或失败的界定很不明显:成功只是意味着没有出现异常,而不表示(重定向的)输出 正确。
使用 doctest
doctest 模块让您可以在文档字符串(docstrings)内嵌入注释以显示各种语句的期望行为,尤其是函数和方法的结果。这样做很像是让文档字符串看起来如同一个交互式 shell 会话;完成这一任务的一个简单方法是,从一个 Python 交互式 shell 中(或者从 Idel、PythonWin、MacPython 或者其他带有交互式会话的 IDE 中)拷贝-粘贴。这一改进的 test_basic.py 脚本举例说明了自诊断功能的添加:
清单 3. 具有自诊断功能的 test_basic.py 脚本
import sys
from gnosis.xml.objectify import XML_Objectify, pyobj_printer, EXPAT, DOM
LF = "\n"
def show(xml_src, parser):
"""Self test using simple or user-specified XML data
>>> xml = '''<?xml version="1.0"?>
... <!DOCTYPE Spam SYSTEM "spam.dtd" >
... <Spam>
... <Eggs>Some text about eggs.</Eggs>
... <MoreSpam>Ode to Spam</MoreSpam>
... </Spam>'''
>>> squeeze = lambda s: s.replace(LF*2,LF).strip()
>>> print squeeze(show(xml,DOM)[0])
-----* _XO_Spam *-----
{Eggs}
PCDATA=Some text about eggs.
{MoreSpam}
PCDATA=Ode to Spam
>>> print squeeze(show(xml,EXPAT)[0])
-----* _XO_Spam *-----
{Eggs}
PCDATA=Some text about eggs.
{MoreSpam}
PCDATA=Ode to Spam
PCDATA=
"""
try:
xml_obj = XML_Objectify(xml_src, parser=parser)
py_obj = xml_obj.make_instance()
return (pyobj_printer(py_obj).encode('UTF-8'),
"++ SUCCESS (using "+parser+")\n")
except:
return ("","++ FAILED (using "+parser+")\n")
if __name__ == "__main__":
if len(sys.argv)==1 or sys.argv[1]=="-v":
import doctest, test_basic
doctest.testmod(test_basic)
elif sys.argv[1] in ('-h','-help','--help'):
print "You may specify XML files to objectify instead of self-test"
print "(Use '-v' for verbose output, otherwise no message means success)"
else:
for filename in sys.argv[1:]:
for parser in (DOM, EXPAT):
output, message = show(filename, parser)
print output
sys.stderr.write(message)
print "="*50
注意,我在经过改进(和扩展)的测试脚本中放入了 main 代码块,这样,如果您在命令行中指定了 XML 文件,脚本将继续执行以前的行为。这样就让您可以继续分析测试用例以外其他的 XML,并只着眼于结果 -- 或者找出 gnosis.xml.objectify 所做事情中的错误,或者只是理解其目的。按标准的方式,您可以使用 -h 或 --help 参数来获得用法的说明。
当不带任何参数(或者带有只被 doctest 使用的 -v 参数)运行 test_basic.py 时,就会发现有趣的新功能。在这个例子中,我们在模块/脚本自身上运行 doctest -- 您可以看到,实际上我们将 test_basic 导入到脚本自己的名称空间中,这样我们可以简单地导入其他希望要测试的模块。 doctest.testmod() 函数去遍历模块本身、它的函数以及它的类中的所有文档字符串,以找出所有类似交互式 shell 会话的内容;在这个例子中,会在 show() 函数中找到这样一个会话。
show() 的文档字符串举例说明了在设计好的 doctest 会话过程中的几个小"陷阱(gotchas)"。不幸的是, doctest 在解析显式会话时,将空行作为会话结束来处理 -- 所以,像 pyobj_printer() 的返回值这样的输出需要加一些保护(be munged slightly)以进行测试。最简单的途径是使用文档字符串本身所定义的像 squeeze() 这样的函数(它只是除去紧跟在后面的换行)。此外,由于文档字符串毕竟是字符串换码(escape),所以 \n 这样的序列被扩展,这样使得在代码示例 内部对换行进行换码稍微有一些混乱。您可以使用 \\n ,不过我发现对 LF 的定义解决了这些问题。
在 show() 的文档字符串中定义的自测试所做的不仅是确保不发生异常(对照于最初的测试脚本)。为正确的"对象化(objectification)"至少要检查一个简单的 XML 文档。当然,仍然有可能不能正确地处理一些其他的 XML 文档 -- 例如,上面我们试过的名称空间 XML 文档 testns.xml 遇到了 EXPAT 解析器失败。由 doctest处理的文档字符串 可能会在其内部包含回溯(traceback),但是在特别的情况下,更好的方法是使用 unittest 。
使用 unittest
另一个包含在 gnosis.xml.objectify 中的测试是 test_expat.py 。创建这一测试的主要原因仅在于,使用 EXPAT 解析器的子软件包用户常常需要调用一个特别的设置函数来启用有名称空间的 XML 文档的处理(这个实际情况是演化来的而不是设计如此,并且以后可能会改变)。老的测试会试图不借助设置去打印对象,如果发生异常则捕获之,然后如果需要的话借助设置再去打印(并给出一个关于所发生事情的消息)。
而如果使用 test_basic.py , test_expat.py 工具让您可以分析 gnosis.xml.objectify 如何去描述一个新奇的 XML 文档。但是与以前一样,有很多我们可能想去验证的具体行为。 test_expat.py 的一个增强的、扩展的版本使用 unittest 来分析各种动作执行时发生的事情,包括持有特定条件或(近似)等式的断言,或出现期望的某些异常。看一看:
清单 4. 自诊断的 test_expat.py 脚本
"Objectify using Expat parser, namespace setup where needed"
import unittest, sys, cStringIO
from os.path import isfile
from gnosis.xml.objectify import make_instance, config_nspace_sep,\
XML_Objectify
BASIC, NS = 'test.xml','testns.xml'
class Prerequisite(unittest.TestCase):
def testHaveLibrary(self):
"Import the gnosis.xml.objectify library"
import gnosis.xml.objectify
def testHaveFiles(self):
"Check for sample XML files, NS and BASIC"
self.failUnless(isfile(BASIC))
self.failUnless(isfile(NS))
class ExpatTest(unittest.TestCase):
def setUp(self):
self.orig_nspace = XML_Objectify.expat_kwargs.get('nspace_sep','')
def testNoNamespace(self):
"Objectify namespace-free XML document"
o = make_instance(BASIC)
def testNamespaceFailure(self):
"Raise SyntaxError on non-setup namespace XML"
self.assertRaises(SyntaxError, make_instance, NS)
def testNamespaceSuccess(self):
"Sucessfully objectify NS after setup"
config_nspace_sep(None)
o = make_instance(NS)
def testNspaceBasic(self):
"Successfully objectify BASIC despite extra setup"
config_nspace_sep(None)
o = make_instance(BASIC)
def tearDown(self):
XML_Objectify.expat_kwargs['nspace_sep'] = self.orig_nspace
if __name__ == '__main__':
if len(sys.argv) == 1:
unittest.main()
elif sys.argv[1] in ('-q','--quiet'):
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(Prerequisite))
suite.addTest(unittest.makeSuite(ExpatTest))
out = cStringIO.StringIO()
results = unittest.TextTestRunner(stream=out).run(suite)
if not results.wasSuccessful():
for failure in results.failures:
print "FAIL:", failure[0]
for error in results.errors:
print "ERROR:", error[0]
elif sys.argv[1].startswith('-'): # pass args to unittest
unittest.main()
else:
from gnosis.xml.objectify import pyobj_printer as show
config_nspace_sep(None)
for fname in sys.argv[1:]:
print show(make_instance(fname)).encode('UTF-8')
使用 unittest 为较简单的 doctest 方式增添了相当多的能力。我们可以将我们的测试分为几个类,每一个类都继承自 unittest.TestCase 。在每一个测试类内部,每一个名称以".test"开始的方法都被认为是另一个测试。为 ExpatTest 定义的两个额外的类很有趣:在每次使用类执行测试前运行 .setUp() ,测试结束时运行 .tearDown() (不管测试是成功、失败还是出现错误)。在我们上面的例子中,我们为专用的 expat_kwargs 字典做了一点簿记以确保每个测试独立地运行。
顺便提一下,失败(failure)和错误(error)之间的区别很重要。一个测试可能会因为一些具体的断言无效而失败(断言方法或者以".fail"开头,或者以".assert"开头)。在某种意义上,失败是期望中的 -- 最起码从某种意义上我们已经具体分析过。另一方面,错误是意外的问题 -- 因为我们事先不知道哪里会出错,我们需要分析实际测试运行中的回溯来诊断这种问题。不过,我们可以设计让失败给出诊断错误的提示。例如,如果 Prerequisite.haveFiles() 失败,将在一些 TestExpat 测试中出现错误;如果前者是成功的,您将不得不到其他地方去查找错误的根源。
在 unittest.TestCase 的继承类中,具体的测试方法中可能会包括一些 .assert...() 或者 .fail...() 方法,但也可能只是具有一系列我们相信应该会成功执行的动作。如果测试方法没有按预期运行,我们将得到一个错误(以及描述这个错误的回溯)。
test_expat.py 中的 main 程序块也值得察看。在最简单的情况下,我们可以只使用 unittest.main() 来运行测试用例,这将断定哪些需要运行。使用这种方式时, unittest 模块将接受一个 -v 选项以给出更详细的输出。根据指定的文件名,在执行了名称空间设置后,我们打印出指定的 XML 文件的表示,从而大致上保持了对此工具稍老版本的向后兼容。
main 中最有趣的分支是期待 -q 或 --quiet 标签的那个分支。如您将期望的,除非发生失败或错误,否则这个分支将是静默的(quiet,即尽量减少输出)。不仅如此,由于它是静默的,它只会为每个问题显示一行关于失败/错误位置的报告,而不是整个诊断回溯。除了对静默输出风格的直接利用以外,这个分支还举例说明了相对于测试套件的自定义测试以及对结果报告的控制。稍微有些长的 unittest.TextTestRunner() 的默认输出被定向到 StringIO out -- 如果您想查看它,欢迎您到 out.getvalue() 去查找。不过, result 对象让我们对全面成功进行测试,如果不是完全成功还可以让我们处理失败和错误。显然,由于它们是变量中的值,您可以轻松地将 result 对象的内容记录入日志,或者在 GUI 中显示,不管怎么样,不是仅仅打印到 STDOUT。
组合测试
可能 unittest 框架最好的特性是让您可以轻松地组合包含不同模块的测试。实际上,如果使用 Python 2.3+,您甚至可以将 doctest 测试转化为 unittest 套件。让我们将到目前为止所创建的测试组合到一个脚本 test_all.py 中(诚然,说它是我们目前为止所做的测试有些夸张):
清单 5. test_all.py 组合了单元测试
"Combine tests for gnosis.xml.objectify package (req 2.3+)"
import unittest, doctest, test_basic, test_expat
suite = doctest.DocTestSuite(test_basic)
suite.addTest(unittest.makeSuite(test_expat.Prerequisite))
suite.addTest(unittest.makeSuite(test_expat.ExpatTest))
unittest.TextTestRunner(verbosity=2).run(suite)
由于 test_expat.py 只是包含测试类,所以它们可以容易地添加到本地的测试套件中。 doctest.DocTestSuite() 函数执行文档字符串测试的转换。让我们来看看 test_all.py 运行时会做什么:
清单 6. 来自 test_all.py 的成功输出
$ python2.3 test_all.py
doctest of test_basic.show ... ok
Check for sample XML files, NS and BASIC ... ok
Import the gnosis.xml.objectify library ... ok
Raise SyntaxError on non-setup namespace XML ... ok
Sucessfully objectify NS after setup ... ok
Objectify namespace-free XML document ... ok
Successfully objectify BASIC despite extra setup ... ok
----------------------------------------------------------------------
Ran 7 tests in 0.052s
OK
注意对执行的测试的描述:在使用 unittest 测试方法的情况下,他们的描述来自于相应的 docstring 函数。如果您没有指定文档字符串,类和方法名被用作最合适的描述。来看一下如果一些测试失败时我们会得到什么,同样有趣(为本文去掉了回溯细节):
清单 7. 当一些测试失败时的结果
$ mv testns.xml testns.xml# && python2.3 test_all.py 2>&1 | head -7
doctest of test_basic.show ... ok
Check for sample XML files, NS and BASIC ... FAIL
Import the gnosis.xml.objectify library ... ok
Raise SyntaxError on non-setup namespace XML ... ERROR
Sucessfully objectify NS after setup ... ERROR
Objectify namespace-free XML document ... ok
Successfully objectify BASIC despite extra setup ... ok
随便提及,这个失败写到 STDERR 的最后一行是"FAILED (failures=1, errors=2)",如果您需要的话这是一个很好的总结(相对于成功时最终的"OK")。
从这里开始
本文向您介绍了 unittest 和 doctest 的一些典型用法,它们已经改进了我自己的软件中的测试。阅读 Python 文档,以深入了解可用于测试套件、测试用例和测试结果的全部范围的方法。它们全部都遵循例子中所描述的模式。
让自己遵从 Python 的标准测试模块规定的方法学是良好的软件实践。测试驱动(test-driven)的开发在很多软件周期中都很流行;不过,显然 Python 是一门适合于测试驱动模型的语言。而且,如果只是考虑软件包更可能按计划工作,一个软件包或库如果伴随有一组周全的测试,会比缺乏这些测试的软件包或库对用户更为有用。
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。