Saturday, August 13, 2005
阅读Ruby Gems项目文档是很有趣的,因为它与Python Eggs在某种程度上有着惊人的相似之处,恩,总之很像我对eggs所期望变成的样子。Eggs有着一大堆面向Python和面向插件(plugin-oriented)的特性,这些特性是gems所没有的。但是,gems却是十分精致:)。需特别指出的是,他们已经解决了基本的代码签名方法,利用工具来列表显示安装过的包和卸载掉的包,他们更是建立了一个内置的web服务器以允许个人共享其本地包。这虽然显得有点疯狂,但很cool。这与PyPI的运行方式即为提供一个包索引在本地文件系统对eggs进行扫描有几分相像。我想那可能会使它启动变得异常缓慢,但利用eggs至少你可以在目录项获得几乎所有有用的信息而不必打开文件。所以,我想实际上你可以得到一个很棒的索引来完成那些事情。
然而现在,对于eggs一个Web服务器并不是优先级最高的。CVS(Concurrent Versions System,并发版本系统)的0.6开发版本正在经受成长之痛,比如需要为pkg_resources模式完成一个基础手册并增加一些关于下载过程的解释性信息到ez_setup和setuptools。还有一些仍在继续着的关于“测试”命令和支持“py.test”的讨论。我想对于py.test遗憾的是它并没有扩展Python的unittest模块。
我以为,unittest的口碑已经不怎么好。可不管你是否喜欢它的基本测试工具,它仍是一个极其优秀的框架。实际上,我认为它是Python标准库中最好的框架之一。它的功能性被很干净地分成了四种角色:运行器(runners),装载器(loaders),例程(cases)和结果(results),它们每一个都可以通过对象继承适合的接口来完成。正因为这种格外干净的分解,其基本框架具有惊人的可扩展性。然而,许多人们并未意识到这一点,所以他们创建了一些充满竞争而且不兼容的框架,比如twisted.trial和py.test。
我并不是说其他框架就不好,我的意思只是额外的功能性的增加通常可以通过实现一个重置的装载器、运行器、例程或结果类来完成,而这依赖于你想要添加什么样的特性。例如,py.test的很多在广告中所声称的特性可以以unittest框架向后兼容的扩展形式来干净地加以实现,然后它可在类似于unittestgui的测试运行器下面来运行,与现有的setuptools和"test"命令干净地整合在一起。
人们有一些抱怨,比如要利用unittest寻找并运行测试,但我想,那是因为他们不了解如何扩展或在恰当的位置首先使用它。今年早些时候,我编了一个只有20行针对unittest的简易“装载器”,它可以扫描名字以“Test”打头的模块的子包(subpackages),并且unittest.main()函数允许你指定想要使用的装载器。因此,通过用命令行传递一个模块或包名,我可以递归地扫描所有针对tests的包而不必另编写一个全新的测试框架来完成这件事。
从关于各种各样unittest替代者的广告宣传中,我推测这已被看成为一个很大的问题。不幸的是,似乎没有人意识到扩展unittest来完成这些事是多么简单的事情。
所以我想,一个简短的指南是很有必要的。一个“运行器”是最高层的器件,它基于结果运行测试和生成报告。一个“结果”对象记录了它们执行时独立测试是成功还是失败,还可能在运行时当它一出现便产生报告;它通常由运行器创建并是运行器的实现细节。例如,有一个TextTestResult类,其输出点(dots)或“ok”信息,还有一个相似的类,在GUI版本里用来更新进度条。一个“装载器”寻找并积累例程来实现为一个TestSuite(一个TestSuite从技术上讲也是一个“例程”,但你将无须子类化(subclass)它,除非你要做一些奇思异想的事,诸如替代默认的gather-then-test的行为以实现py.test-style的增量搜集)。在做其他一些事情时,默认的装载器可以为例程测试类(test case classes)扫描一个模块,或运行一个函数,并返回一个例程或例程的套件(suit of cases)。
与一些看起来很流行的说法相反,你没必要子类化(subclass)任何一个基于这些想法的特殊的unittest实现。(默认的装载器使用isinstance()和issubclass()来确定一个模块中的测试例程,然而如需要,在一个自定义装载器中改变它也很容易)一个“例程”对象只需实现__call__(result), __str__(),和shortDescription()就可与运行器达到完全的兼容。__call__ method应该调用result.startTest(case), result.stopTest(case),并在这两者之间适当地调用result.addSuccess(case),result.addError(case,sys.exc_info())或result.addFailure(case, sys.exc_info())的其中之一。剩下的就取决于要管理的例程了。
实现一个自定义装载器的方法只有“loadTestsFromName”和“loadTestsFromNames”。然而,如果你子类化默认的装载器类(unittest.TestLoader)你可以选择性地覆盖其功能性的各个方面。对于我那个递归扫描的装载器,我只覆盖了“loadTestsFromModule”方法,以便让它检查该模块是否是一个包,并且之后也搜索一些子包和任何名字以“Test”为首所包含的模块。这比起写一个完整的自定义装载器要简单多了,但如果我想要构造一个py.test-like测试查找算法,我将必定以同样的方式实现,子类化TestLoader并添加新的功能特性。那是因为创建一个自定义脚本以利用一个自定义装载器来运行unittests只需如下一些代码:“from unittest import main; main(module=None, testLoader=MyCustomLoader())”。那并不很难,不是吗?
如果我也想改变进度报告,我需要创建一个新的“结果”类然后子类化TextTestRunner,覆盖_makeResult()返回一个我改变过的结果类的实例。unittest.main()也接受一个“testRunner”关键字参数,所以,那也许还需要添加一行代码到我的脚本中。
简而言之,没有什么理由来创建一个全新的框架。我们现有的这个就已经很好了,即使它的默认特性也许不会合每一个人的口味。然而,它是一个框架,这就意味着你应该往它的里面放一些东西。因为它是一个标准框架,这将有机会让我们所有的测试工具在一起运行,而不是强迫人们将他们的测试临时组合在一起。Python2.4的doctest模块提供API来创建来自doctest兼容unittest的测试例程,这就意味着我可以与已有的800+基于unittest的测试例程协同地使用doctest。而利用py.test、twisted.trial或任何其他从零开始实现的框架是无法做到这一点的,这类从零起实现的框架是抛弃了unittest模块的最高级设计了的。unittest或许是一个普普通通的工具,然而它更是一个允许我们开发可协同工作工具的优秀框架,这与WSGI允许在web应用程序与web服务器间实现协同工作很相像。unittest理应得到比它所得到的更多的重视,因为正是它,结束了与我们所拥有彼此不兼容的web框架一样之多的互不兼容的测试框架的日子。
(原文链接网址:http://dirtsimple.org/2005/08/ruby-gems-python-eggs-and-beauty-of.html)