InfoQ.com 最近采访了《新一代Java 测试:TestNG 和高阶理念》的作者Hani Suleiman 和Cédric Beust,就这本书的内容以及他们对测试的总体看法进行了讨论。InfoQ 很荣幸在此给大家提供该书第二章的节选,“Mocks 和Stubs”。
您可否对那些不熟悉相关“历史”故事的读者谈一下激发您编写TestNG 的缘由?
TestNG 是作为一个实验性项目而启动的。当时的主要动机是:虽然 JUint 已经被使用好多年了,但有许多 JUnit 所采用的概念和方式让我觉得不太“舒服”。随着进一步的探索,我愈发不敢苟同它一些核心的设计哲学,比如在每个方法调用之前需要重构你的类实例(以至于如果我想在各调用间维护状态的话,就不得不使用静态);再比如它不允许创建依赖性测试,而这点对功能测试来说却是至关重要的。
总之,我觉得 JUnit 在单元测试方面做的非常好,但在各种功能性测试和验收测试方面却有很多缺陷。
话说回来,我也使用过 JSR 注解。在我看来,注解确实对标注测试函数很实用。
最后一个理由是来自于一个有着怪名字的网站,叫作 del.icio.us ,他们引入了一个奇异的称为“标签(tags)”的新概念。我发现如果对测试函数加标签的话就显得非常整洁,并且不用重新编译就可以对这些函数进行任何组合调用。我把这些标签重新命名为“组合(groups)”,在使用的同时,我发现这些“组合”对注解也非常适用。
正是这些想法促使我在一年前着手创建 TestNG 1.0,我只是觉得好玩才将它发布,而且获得了许多反馈。
在撰写新一代 Java 测试工具的时候,你想要实现怎样的目标?
令我非常惊讶的是,TestNG 获得了很大认同。略去 JUnit 在 Java 测试领域所存在的垄断不提,一个非常大的社区尝试着采用 TestNG,并开始将它推向其它测试领域。在最初几个月里,我几乎无法跟上列表中的新功能需求。这让我觉得背后似乎有一个强大的 Java 开发队伍,这些人对 JUnit 忍气吞声了那么多年,终于抓到一个可以参与到创建新一代的测试框架的机会,热情全然迸发。
而且,幸运的是,我很快加入了 Alexandru Popescu 的项目。我们一起开始实现,并和 TestNG 的用户们讨论各种各样的新点子。今天大家所看到的 TestNG 其实完全是由它的用户来设计的,Alexandru 和我只是帮助将它实现了而已。
目前,TestNG 群组大概有 1300 多人注册。在过去的几年中,我们试图抓住邮件列表讨论中所有的智慧、所有有趣的点子,现在终于初见成效。我曾几次试图写书,但都没写成。要么是因为没有时间,要么是觉得自己还没有足够的能量来肩负起这个重担。
我必须感谢 Hani,最终是他的自告奋勇才把我推到了“前线”。忽然之间,整个项目看上去更加可行,并且基于这么些年来对 Hani 的认识,我对完成这份可能对 Java 测试社区非常有用、极具价值、极具改革性的资料充满了信心。
对一些读者来说,他们可能只是从 BileBlog 认识了 Hani,Hani 本人是怎样和你一起工作的呢?
事实上,Hani 在现实生活中表现和读者从他的博客上所感受到的迥然不同。很多人还没有认识到 Bile Blog 其实只是一幕话剧。Hani 创造了一个在线人物性格,因而他可以以一种完全无法确认的方式来发表他的想法,但还是有很多人没有透过语言看到他的庐山真面目。
但一旦看透他的语言,就会发现在每一篇博文背后都蕴藏着他实实在在的第一手经验。由于他仅仅讨论一些他个人接触的话题和框架(很多时候,还都是让他遭受不愉快的事物),所以他的博客没有一点儿无端造谣或者扭曲事实的东西。
我很快在他的博文中发现了这个特征,并且他很多总结都激起了我的共鸣,所以和很多人一样,我很期望能够和他见面。他的表现却和我的预料相去甚远。他态度温和、说话柔和并且极有礼貌,但只要你问及关于技术的问题,他的用词就极端尖锐、一针见血,但肯定没有任何恶言恶语。他的言语也显示出,他拥有深厚扎实的 Java 及相关领域的知识功底;拥有批评性且充满好奇的思维;以及大型软件系统如何工作的第一手实战经验。
Hani 和我都是实用主义者:我们喜欢客观地考虑问题,试图在没有任何个人偏见的前提下权衡各个选项来解决问题。我们在本书的每个章节都非常卖力地传递着这种客观性(除了名为“Digressions”的章节以外。在这个特别的章节,我们让自己在一些特殊的话题上自由地进行讨论。BileBlog 的粉丝们可能会更喜欢这个章节:))。
在测试代码方面,你觉得开发人员可能犯的最常见的错误是什么?
在他们常犯的错误中,首当其冲的是:编写代码时忽略了测试。无论你编写测试代码是多么的得心应手,只要你所要测试的代码在编写时没有考虑到测试的话,你的工作就会陷入严重困境,除非你进行重构(但重构不是一直可以做的)。
如果你能同时处理代码和测试(这是很令人羡慕的情况),只要你很早就开始考虑如何将你的代码变得可测试,你的工作就会变得轻松得多。很多人都把这句话理解为“测试驱动的开发是唯一正确的编程方式”,但 Hani 和我极不同意这种极端主义。测试驱动开发是有用的,但在编写测试代码的时候,还有其它一些简单的原则你必须铭记于心,你必须保证测试代码干净并且同样可测。
为了满足网站读者的好奇必,我在这里先列出几条关于如何令代码变得更加可测试的看法:
- 避免静态。非常不幸,由于存在很多难测的代码,使 singleton 和近似的模式把静态变得相当受欢迎。解决这个问题的一个方法是使用依赖注射框架(Hani 和我就很推崇 Guice,我们在书中有提到,但 Spring 也是一个很好的选择)。
- 别再对“修改代码使之更易测试”而犹豫不决。如果这个修改意味着要将 private 函数改成 protected 或者 public,这常常是个可行的好主意。因为“激活自动测试能力”所得到的收益,往往要比“将函数变得更可见带来的潜在维护成本”要大得多。
- 接口并没有像很多人希望你所相信得那样糟糕。很多 XP 开发员会跟你说不要引入任何接口,除非你确定至少会有两个类来实现该接口,但我们的处境是每个具体类潜在由两种不同的方式使用:产品中或者测试中。抽取出接口常常是使得你的代码更干净利落更易测试的举动。
当然,我们在书中就这些问题以及其他问题进行了深入的讨论。
在您看来,什么样的代码最难测试?
最早浮现在我脑海的两个例子是界面接口和移动软件。
一直以来,界面和移动接口很难测试,因为它们的 API 在设计的时候从来没有考虑到自动测试性。界面领域已经得到了巨大的进步(如果你需要测试 Swing 或 SWT 代码的话,请参考示例 FEST),但 Java ME 仍然极为糟糕,Java 移动普遍来说很糟糕。移动领域的主要问题是模拟器是个黑盒,无法从外部驱动,从而很难在运行软件上做功能测试。并且,MIDP 和 Java ME 类继承的设计很烂,在最近的 JSR 中也没有看到其任何优化的迹象。
我希望,我所工作的这个项目(Google Android)能够解决这些问题,我们尽了最大的努力来将可测试化和健全明智的设计置于首位。如果你不同意这个观点的话请告诉我:)。
在软件服务器上运行的测试代码远根本不是什么芝麻小事,但我们最近在这点上取得了很大的进步,而且现在我们对如何借助多个框架来实现这些进步有了一个全面的理解,而这些框架在过去五年中的开发也正是为了这个目的。
您认为测试领域和 TestNG 未来将如何发展?
我想,测试领域在过去的五年里向前跨出了一大步。这期间,越来越多的开发人员意识到,自动化测试可能会成为他们的工作的一部分。可就在不久前,很多开发人员还不屑于编写测试代码。他们觉得写测试代码既浪费时间又浪费他们的才能。而十年前,测试只是分派给一个可有可无的 QA 团队的任务,开发人员甚至尽量避免跟 QA 团队打交道。再向前追溯到更早一些,很多教我的计算机老师几乎也从来没有编写过测试代码。我们一味地投入到项目和程序中去,而他们则用一张清单来验证我们所做的东西是否正确地覆盖了所有要点。
近几年,一切都发生了巨大的变化。而展望未来,终有一天我们会看到:提交无测试的代码就跟提交无法编译的代码一样糟糕。
总而言之,我相信,大部分现代语言(Java、C#、Ruby、甚至还有未来不太明确的 Visual Basic :))都覆盖了测试部分。框架就在那里,而且常常都非常灵活,但我们缺乏的仍然是工具。在过去的几年里,我们取得了很多进步,但我们仍然缺少一个环境,一个能够及时告诉你刚写的代码是否正确的环境;一个可以分析你的代码并对改进和测试方式提供建议的环境。我们需要这样的及时提示是因为如果代码不正确的话会中断整个测试。
我觉得在最近几年内,测试领域最有意思的进步将会是测试工具的进步,我几乎已经急不可待了。
在撰写新一代 Java 测试的时候,你想要实现什么样的目标呢?
让我一次次受到震撼的是,在专业环境中,测试所占的一席之地是多么狭小。我接触到的大部分单元测试都来自于开源项目,而那种无关联性又常常困扰着我。
时间一长,我不得不承认:测试实际上无法满足我所在的项目的需要(主要是企业应用)。企业应用系统太过负载,不容易测试,这实在是个不幸。
在 TestNG 的开发中,我开始和 Cedric 讨论这一点。我们所产生的共鸣是我们都很倾向于激活测试,而不是被强迫以某种方式测试。依赖性没有什么可耻的,状态性测试也合情合理。事实上,有些时候,这些是正确的前进方向,还有其它一些类似的原则也一样,都不是默守陈规、因循守旧的智慧。
渐渐地,“可测试性”这个模糊的概念开始凝聚升华。测试代码中开始浮现出一些模式,但在我们试图观察别人都在做什么的时候,在现实世界中却只发现了很少几个实例,并且这些例子常常都很微不足道,无法真正说明在面对实际问题的时候该如何运用。
我曾经和很多从事测试工作的技术人员进行过讨论,他们在工作中所运用到的技术和设计常常给我我留下深刻的印象。
然而,似乎只有很少一部分杰出的技术人员掌握了这些技巧。在那些认识到测试重要性的人群(几乎是所有开发人员)和那些真正成功地将其应用于开发过程的开发人员之间有个巨大的沟壑。目前几乎很难找到全面的指南来帮助那些认识到重要性的开发员成为后者。于是,本书应运而生。
在测试代码方面,你觉得开发人员可能犯的最常见的错误是什么?
人们总是设想所有的测试应该短小快速。对于很少一部分软件来说,这是对的。但如果测试需要运行很长的时间也并不为过。如果你需要一个真正的数据库,那么做依赖于其他测试的状态性测试是无可厚非的。
在您看来,什么类型的代码最难测试?
除 Cedric 的几点看法以外,我还想再增加一项,那就是数据库测试。的确,有很多工具能够帮助你完成数据库测试,也有很多的技术能够帮助你简化一些相关难题,但我尚未听说有一套好的解决方案,用于为迁移数据而编写测试。如果你根据一组静态数据写测试的话,那么你就会逐渐与真正运行中数据库背道而驰。如果你使用运行中的备份数据的话,那你就不得不处理安全因素,而且你必须保证没有将敏感数据暴露给开发人员。在银行业,产品世界更是和开发有着严格的分隔。所以在保证数据库测试普遍有用性的同时又能与自动测试并发,是需要很多技巧的。
很多开发人员在编写测试代码时,没考虑到“覆盖性”。为什么在您看来覆盖性非常必要呢?
那些整天操心覆盖性的开发人员当然会觉得少不了它!覆盖性只能给你粗略地指出哪里还没测试,覆盖率的具体数值几乎完全没有意义。本书还讨论了那些令我们过分陶醉的红 / 绿条的"邪恶"之处,还讨论了抗拒那种让红条越来越短的诱惑有多重要。
当代码覆盖能够反映出你的测试代码确实正按你命令行事的时候,它们是有用的。但更重要的是,我们必须铭记,覆盖性报告和功能遗漏完全无关,因为代码不是首先被检验的东西,它只是我们测试工具箱中另一样可以被利用的工具,用好了事半功倍,不恰当的使用则事倍功半。如果利用它竭尽全力地检验你的代码是否被完全覆盖(甚至详细节到在你的代码中加入注释,让你的覆盖工具忽略某些部分),这就不是什么恰当的用法。正确的用法应该是:利用它来确认测试并且试图标识出那些需要进一步研究的部分。本书中就详细讨论了如何适当地使用代码覆盖工具。
您撰写了关于 JEE 测试的章节,提到了像 JPA、JDBC 和 JNDI 等这些话题。我知道很多开发人员都觉得这些测试“从创建到使用”都很难掌握。您又是怎样来平衡那些需要完整的容器、数据库的端到端(end to end)测试和用 Mocks 替换关键的 JEE 构件而进行的轻量级测试的呢?
我极不赞成用 mocks 来模拟 Java EE 构件。这些 API 常常很复杂,而且需要很多测试来验证兼容性。终端用户无法看到为了让某个产品和特定的 JAVA EE API 兼容要做多少麻烦的验证工作。而 Mock 实现几乎是完全没经过验证的。随着时间的推移,使用 Mock 实现带来的风险是:人们开始实现越来越多的 API,也就越来越多有可能出错,单纯为了让测试好看而编写的代码也越来越多。最后 Mock 实现也就愈来愈背离真正的实现。
我个人认为,对于测试这样的代码,重构是远胜于 mocks 的工具。在本书中,我们还列举了一个通过重构来修改登录 servlet 使之变得可测试化的例子。
虽然我前面反对用 mocks,但 mocks 和 stub 对象荏苒各有它们的用处。比如如果 API 实在很微小,或者如果牵涉到第三方依赖的话,你就无法做重构,那么 mocks 就是最佳选择了。
在关于集成的章节中,您讲到了 Swing UI 和 Selenuim 测试。在您的项目中,您是如何成功地运用 UI 驱动测试的呢?一些开发人员认为不值得为可能发现的有限的 bug 而花时间来编写这样的测试代码。你怎么看这个问题?
我必须诚实地告诉你答案是“不”。我同意那些开发人员所说的,这些花销可能不值得。精确的布局检查这类测试现在还是由真人来完成比较好。在我们投入的 RIA 世界和 AJAX 应用中以后,情况有所改变。在 RIA 和 AJAX 的应用中,经常使用 Javascript 来处理很多逻辑,所以对这些 Javascript 代码进行测试也很重要。通常来说,在 Java 中,编写可测试代码的原则同样适用 Javascript。比如不要将表示和业务逻辑混杂在一起;尽量是使用独立控件,避免依赖,等等。对于这类应用程序的测试可以是结合 Javascript 测试文件、基于浏览器的测试。
评论