2008年12月28日星期日

功能测试之美

功能测试之乐:

功能测试定义了产品的业务需求,通过它业务人员可以了解系统是否能在各个业务场景下正常工作。功能测试通常使用某种自动化测试框架编写,这样开发者可以从自动化的功能测试中获得快速反馈,为下阶段新功能的开发或软件内部实现的重构提供帮助。另一方面,它大大减少了手动环节可能引入的错误,而将枯燥的回归测试交给机器完成,在加快测试速度的同时,将质量保证人员解放出来,从而使他们可以更多地关注于创造性的探索测试。通过从用户角度进行的功能测试,团队对系统在真实条件下的可用性充满信心,而自动化的功能测试也大大提高了工作效率。这样一来,产品能以更高的质量,更快的速度进入市场。

功能测试之苦:

* 保持验收条件及其技术实现的同步

在任何开展自动化功能测试的敏捷开发团队中通常会存在两套系统、 一套是BA(业务分析师)\QA(质量保证人员)所编写、维护的验收条件,可能以纸卡、Wiki等形式记录下来。 另一套是验收条件的技术实现。以源代码的形式记录,由开发者维护。两套系统的并行发展, 带来了同步的问题。如何快速将验收条件转变为技术实现?当验收条件变化时如何使源代码部分同步变化?可不可以将验收条件与测试关联起来,利用Rspec的思路消除这个并行的系统?

* 无法系统化的划分测试

速度是阻碍频繁运行功能测试的主要原因。进行功能测试的团队常常花费数十分钟甚至数小时来运行完整的功能测试套件,加快测试几乎总以失败而告终,从用户角度进行业务场景的测试决定了功能测试的速度天生就是缓慢的。随着软件功能的日益完善,更多的测试被添加到套件中,庞大的套件也使得测试运行的时间越来越长。无法快速得到反馈使团队没有安全感,同时大大减缓开发的脚步,烦躁的开发者甚至开始逃避运行测试,将不安全的代码集成到产品中。

只运行相关的测试,听起来这似乎是一个解决方案。当开发者修改了登录模块的实现后,为什么他们非得花费1个小时等待其他模块的测试结果呢?如果可以仅仅运行登录模块相关的测试,将其余的测试留给持续集成工具运行,开发的效率将大大提高。但遗憾的是在XUnit的世界中,系统化的划分测试并不是件容易的事情。 不论是利用文件名和目录来区分,还是手工维护测试套件,最终总会变成难以维护的大泥球。

* 阅读测试花费的大量时间

大量测试代码总是难以阅读。随着项目的进行, 各种不同习惯的缩写出现在代码中、 测试代码中出现的大量的方法、设置数据越来越复杂等都给阅读测试带来了极大的麻烦。面对失败的测试,试图修复它的开发者总是需要在复杂的代码中挣扎着找出测试的意图,过滤掉准备数据的过程,抽丝剥茧地找到这个测试所覆盖的业务流程,分析究竟是哪个产品模块出了问题。出现这样问题的根源是没有对测试的目的(业务价值)和技术实现做出合理的抽象。

能否有这样一个视图,过滤掉所有让人分散注意力的方法(私有方法,准备数据、清理数据的方法等)让开发者可以清楚地看到测试的目的和步骤,甚至让测试以一种自然语言的形式展现出来,让非技术人员也可以轻易地阅读修改测试呢?


* 启动功能测试的花费

在任何一个团队开始功能测试的第一步,总是耗时和痛苦的。下载各种依赖的库文件,在脚本中进行配置, 保证功能测试可以在命令行执行, 同时还要对IDE进行配置,让开发者可以从IDE中方便地运行单个测试。

能不能缩短这个过程,减轻开发者的痛苦?

功能测试工具的趋势:

敏捷联盟在2007年10月召开了专题讨论来展望下一代的功能测试工具。在这次讨论中, 功能测试领域的专家们提出了对下一代功能测试工具的展望:

* 需要一个整合的开发环境帮助团队:重构测试元素、自动完成命令、增量式的语法检验(基于测试领域特定语言)、快捷键支持、调试等等
* 需要更具描述性的测试领域语言,如将可执行文件、文字、表格、图片、颜色整合到一个测试用例中
* 需要特定的测试领域语言使测试更具阅读性并容易维护
* 需要具备可以使用多种方式查看/导航测试的能力,来帮助我们了解某个部分与整个领域或者特性之间的联系;将测试按照领域上下文来组织;按照用户定义的关键字进行搜索(跨横切关注点)

Twist

经历过很多的功能测试之苦,我们团队尝试了使用Twist来编写、管理功能测试,并得到了很好的效果。它是ThoughtWorks Studio为软件团队设计开发的下一代协作功能测试平台,提供了一个编写、执行以及维护测试的丰富环境。Twist以Eclipse插件的形式设计并开发,充分利用了Eclipse强大的编辑功能, 支持测试专家们通过DSL来表达测试意图。通过自动完成功能,开发人员可以利用Eclipse中广为人知的CTRL + 1 (帮助完成)来快速地将验收条件转换为相应的技术实现(测试类和方法)。由于Twist中的Driver是以Spring Bean的形式注入到测试中,开发者可以以Twist作为平台,使用任何熟悉的DRIVER(如Selenium、Watir、fraiksen)来编写测试。

那么Twist是怎样帮助团队来减轻测试之苦的呢?

* 保持验收条件以及其技术实现的同步
之前我们将验收条件保存在Mingle中的故事卡里,而现在QA和BA通过Twist来编写验收条件,并标记为in-progress。在实现的时候,开发者通过IDE来自动生成测试。在任何人修改验收条件或者技术实现时,IDE的重构功能将自动完成它们之间的同步。例如:测试专家们在Twist IDE 中可能写出如下的验收条件:

Search and buy book :
# search for book written by "Martin Fowler"
# add book "refactoring" to shopping cart
# check book "refactoring" is in my shopping cart


开发者通过TWIST IDE, 可以生成如下测试代码(以Java为例)

public SearchAndBuyBook {

public void searchForBookWrittenBy(String name) {
}

public void addBookToShoppingCart(String name) {
}

public void checkBookIsInMyShoppingCart(String name) {
}
}


之后开发人员可以选择通过手工编写代码(例如通过使用Selenium、 watir作为Driver )或者录制的方式(Twist会记录鼠标、键盘在WEB页面上的动作并转变为相应的测试脚本)来实现功能测试。 更方便的是, 测试专家们编写的DSL验收条件在保持可读性的同时,可以像代码一样自动完成、对重构更加友好、并且可以方便的运行。

在开发者对相应的源代码进行诸如重命名或者引入参数(Introduce Parameter)等重构操作时,相应的测试DSL也会被改变.


* 对测试进行标签管理
Twist使用标签(Tag)对测试进行管理,开发团队可以使用任意标签来标注测试。例如:已完成还是未完成测试、对应的故事卡号、相应或者相关的模块名、属于回归测试还是冒烟测试。这样团队可以方便地划分测试。通过使用Twist发布的ant target中的tag属性,你可以轻松地分组运行测试。比如在我们的项目中最广泛使用的一个Tag是in-progress,这样,QA、BA可以随时提交他们的验收条件,这些尚未完成的验收条件(没有相应的技术实现)会被自动过滤掉,不会引起测试的失败。


*更有效率地 阅读测试
Twist很好地在产品中抽象了验收条件和技术实现,并将它们巧妙地关联起来。这样开发者可以通过易于阅读的DSL来快速了解上下文,并通过Eclipse的快捷键F3,快速地在验收条件和技术实现中进行切换,从而更有效率地阅读测试。

* 减少启动功能测试的花费
用户可以通过在Eclipse中创建Twist项目,快速展开测试。 Twist项目包含了运行Twist测试所必需的所有的第三方库。同时,Twist发布了相应的ANT target,大大减少了团队用于启动功能测试的时间。


在使用Twist的过程中,我们也发现了一些问题,主要集中在Twist的IDE不够稳定,会有一些UI的异常等方面。我们已经将BUG提交到了Twist团队的论坛里,相信稍后的版本会更加稳定易用。 通过使用Twist,很好地将团队以功能测试为中心整合在一起,团队中的所有角色可以通过一套IDE来编写、实现、运行、维护测试,大大减少了交流成本,提高了开发的速度,体会了开发之乐。

2008年12月19日星期五

如何正确的使用Mock

首先我不是反Mock者,但确实对使用Mock持比较审慎的态度,因为Mock是非常难于正确使用的, mock最常见的问题在于假设!假设!假设!

有这样一个功能,当有工作的时候,公民需要买需要买医疗保险,住房公积金和养老保险,如果失业了他只需要买养老保险:


public void requreInsurance(Insurances insurances) {
if (people.getJob() == null) {
insurances.add(new RetirementInsurance());
} else {
insurances.add(new HealthInsurance());
insurances.add(new RetirementInsurance());
insurances.add(new HouseFund());
insurances.add(new UnemploymentInsurance());
}
}


相应的Mock测试有两种情况需要覆盖:

* mock people对象, 假设getJob方法返回null,验证insurances中只有包括养老保险
* mock people对象, 假设getJob方法返回Not null,验证insurances中包含四金


在这里的Mock测试进行了假设,它的coorelation people对象在有工作的时候返回非Null的Job对象,而在没有工作的时候返回Null,类似的代码在任何一个项目中都可以找得到踪迹。

问题在于这样的假设可以被悄无声息的破坏掉,假如有人重构了People对象, 在没有工作时返回一个new NullJob()对象(Null Object Pattern), 这样重构后,失业的人也不得不买四金了,然而我们之前编写的mock测试会100%的通过

如何解决这样的问题呢?

功能测试是一个解决思路,因为在Mock测试中,我们不断的在层与层之间做出假设,一定需要一个端到端的测试来验证我们的假设是否正确。功能测试解决了部分的问题, 回顾上面的问题,你发现至少需要编写两个功能测试才能百分之百的发现刚才重构引入的bug, 如果只编写了happen path的功能测试(有People工作的功能测试),那你只有祈祷QA能帮你及时的找到问题了。 不幸的是,由于功能测试的代价比较大,所以大多数的人都只会编写有限的功能测试,往往这些测试仅仅用于覆盖Happy path. 对于如此简单的问题功能测试尚且不能解决问题,更遑论我们“大型企业级超复杂”的项目呢。

另一个方法就是减少假设,回顾一下我们的实现代码:

if (people.getJob() == null) {
....
} else {
.....
}


getJob的返回值是我们需要进行两次假设的根源,如果没有返回会怎样?


public void requreInsurance(Insurances insurances) {
people.requreInsurance(insurances);
}

public clas People {
public void requreInsurance(Insurances insurances) {
job.requreInsurance(insurances);
}
}

public class Job {
public void requreInsurance(Insurances insurances) {
insurances.add(new HealthInsurance());
insurances.add(new RetirementInsurance());
insurances.add(new HouseFund());
insurances.add(new UnemploymentInsurance());
}
}

public class NullJob {
public void requreInsurance(Insurances insurances) {
insurances.add(new RetirementInsurance());
}
}

在第一个例子中我们需要进行两个假设(Null 和 Not Null),而第二个例子,我们只需要一种假设

第一个例子中稀松平常的代码违反了基本的面向对象的原则“封装”,简而言之是tell do not ask!!原则,因为违反了这个原则,我们不得不做出很多假设,减少假设的一个有效途径就是减少return,tell你的对象替你效劳,如果你对写不出健壮的mock测试烦恼,不妨看看是否写出了符合面向对象原则的产品代码,

正确使用Mock的原则就是尽量不用,改善设计才是王道,代码难于使用通常的方法进行测试而不得不使用mock,绝对是一种smell,不要使用mock来掩盖这种味道。

最后推荐李晓同学的

不要把Mock当作你的设计利器


还有感谢Chris Stevenson今天的Session和帮助。

2008年12月13日星期六

利用mercurial bisect 二分查找bug

前几天QA找到了一个严重的bug,我们编写了一个测试,并修复了它。 问题是我们在哪个版本引入了这个bug? 在1.0版本的时候,这个功能还是正常工作的,

从1.0版本到现在我们进行了上千次提交,找到引入问题的那个版本,看起来是"不可能"的任务,但是因为mercurial提供了bisect,而变的非常容易。

bisect是一个利用二分法来查找在哪个版本中引入bug的一种方法。大致的思路是先标记一个已知没有bug的版本为"good", 再标记一个已知有bug的版本为"bad", 然后hg会自动将版本更新到这两个版本的中间,然后开发者通过运行测试(自动或者手工)决定标记这个版本是"good"或者"bad", hg会再次决定一个中间版本让我们进行测试,直至找到一个相邻的good版本和bad版本,这个bad版本就是我们引入bug的版本。

举个例子:

假如版本1是没有bug的版本,版本10是有bug的版本,我们是在版本7引入了这样一个bug。hg寻找它的办法是:


hg bisect init (初始化数据库)
hg bisect bad R10 (标记版本10是有bug的版本)
hg bisect good R1 (标记版本1是没有bug的版本)


然后,hg会自动更新到R1-R10的中间版本R5,通过运行测试,我们知道这是一个没有bug的版本,于是标记这个版本是没有bug的
hg bisect good


接着,hg自动更新到版本R5-R10的中间版本R8,通过运行测试,我们知道这是一个有bug的版本,于是标记这个版本为bad
hg bisect bad


接着,hg自动更新到版本R5-R8的中间版本R6,通过运行测试,我们知道这是一个没有bug的版本,标记这个版本为good
hg bisect good


hg自动更新到版本R6-R8的中间版本R7,通过运行测试,我们知道这是一个有bug的版本,标记这个版本
hg bisect bad


这样,最后hg通过2分法不断缩小范围,我们得到了这样一个结果

R7 bad
R6 good


这样我们可以得知在R7引入了bug,再分析R7所提交的文件就可以明白当时的错误在哪里了。

这样通过自动化测试和mercurial提供的二分查找,我们可以轻松的找到错误的根源。

2008年12月9日星期二

使用Mercurial Queues

昨天在聊持续集成的时候,同事提出一个问题,因为提交很频繁,能不能让持续集成工具忽略掉某些提交,它们只是完成故事若干步骤的一步,让持续集成工具花1个小时来对明知道不需要的版本进行构建太费时间了。

在ThoughtWorks,很多同事都和我一样患有“频繁提交沉迷综合症”,具体症状是每隔30分钟就得运行提交命令, 否则就血压升高,眼眶发干,手心出汗,焦虑...等。 可是偏偏有时候提交不了,要想提交还得修改不知道多少个文件,你有点恐惧了,可还得硬着头皮干。

正如当时在场的同事田乐回答的一样:根本问题不是持续集成工具是否应该支持这样的功能,而是团队选择的版本控制工具(譬如SVN, CVS, P4)对频繁提交没有提供良好的支持。

我们团队使用的是分布式版本管理工具Mercurial,它医好了我的“频繁提交沉迷综合症”,我把药方写在这里以厮患者。

首先编辑.hgrc文件(* mac和linux用户运行vi ~/.hgrc),在[extensions]条目下加入hgext.mq打开mq的扩展。
[extensions]
hgext.mq =


保存退出,运行

hg --help


你可以在Console上看到新增了很多queue相关的命令

qapplied
qclone
qcommit
qdelete
qdiff
qfold



在mq打开后,如果我们完成了部分修改,可以运行

*hg st (查看本地修改,你应该看到所有被修改过的文件)
*hg qnew -m "[message]" -f "[patch name]" (在queue中压入一条changeset)
*hg st (查看本地修改,现在你应该重新得到了干净的本地目录)


这样在开发的时候我们可以通过hg qnew命令将baby step不断提交,保持一个干净的工作目录。

但是这些changeset是没有提交到主repository上的。在完成了所有的所有的工作后,

我们可以运行
*hg qremove -r qbase:qtip
*hg push
这样,团队中的其他开发人员就可以得到你的修改了。

在我们运行hg qnew后,hg其实帮我们创建了一个标准的patch文件,并把它保存到了
工作目录/.hg/patches


如果你想回家继续开发,可以拷贝走这些patch文件,在另外一台机器上,使用hg qimport或者patch(linux,mac)都可以重建你的工作目录。

如果你有兴趣,请参看mercurial mq的文档

2008年12月3日星期三

真实!真实! 真实!!!

昨天Cruise的功能测试通过了,四十多天来的第一次,团队所有成员都欢呼雀跃,为了这此通过,无数随机失败的测试被找出来,一一修复。 为了让测试运行的速度更快,我们新建了开源的项目test-load-balancer,为了让测试可读性更好,我们扩展了harmcrast。为了应付在不同平台下的测试,我写了Prerequisite

有同事问我,让测试通过很难么? 很难,我们花了40天才得到了一次通过的构建, 我们花了3个月的时间,理清了功能测试的思路,将原来的测试一步步移植到了Twist上,我们花了超过1年的时间从之前失败的经验里汲取教训,学习如何进行测试。值得么? 值得!现在你能站在团队成员身边就能感受到满满的信心,很多信心。 开发变成了一种享受。

变化的起因是:真实!真实! 真实!!! 在尽量真实的环境下测试。

谈起单元测试似乎就离不开Mock,在Mock的环境中搭建的测试就好似沙上建塔,很漂亮,很快,却经不起真实环境的推敲。 Cruise需要跟不同的版本管理工具交互,最实用的方法就是在测试中启动一个版本管理工具的服务器,然后一一测试checkout, sync等功能。没错,它很慢,非常慢。我们有运行时间超过一分钟的单元测试。但是这些测试让我们学习到了WINDOWS, REDHAT, MACOS在某些操作上令人惊诧的不同。 更重要的是,当它们通过的时候,我很有信心,我相信Cruise在与这些版本工具集成方面不会有问题。

使用Mock的最佳实践就是不要使用Mock(李晓 不要把Mock当作你的设计利器), 改进设计才是王道。在我们几乎清理了项目中所有Mock的时候,本地构建时间已经超过了20分钟,全部构建时间在一个半小时左右。后果是我们不得不编写了test-load-balancer,因为一样的原因mingle团队诞生了dtr。 从另一个角度看,让我对Cruise(可以进行并行构建的持续集成工具)的未来充满信心,长时间的构建不可避免,Cruise可以提供成本最低的加快构建的解决方案。

再来说说功能测试,我们之前的方案是在测试环境下启动产品(调整产品的某些参数,调整产品的启动方式等),通过使用数据库设置测试必须的数据环境,然后通过Selenium进行页面操作和验证,测试结束后利用DBUNIT来清理环境。 这样的测试除了验证了测试环境的产品能正常工作外,什么也说明不了。这也是为什么以前虽然也有很多测试,我们却在发布前依然诚惶诚恐。

我承认,使用真实的产品进行测试很复杂,因为很多测试必须的数据设置和查询都难以在外部完成。但是这样单一的,封闭的使用软件的方式是我们希望的么?开放的产品(基于Open API + Restful的设计), 让测试变的更容易,同时也给用户带来了好处(界面进行的重复操作可以通过脚本自动化)。

我们现在的方案是直接在产品包上进行测试,数据的设置通过REST URL进行,同时利用公布出来的JSON来判断产品是否已经被设置为我们希望的状态,再利用selenium进行页面的操作。在这组测试完成时,我们可以对最终交付的软件的质量做到心中有数。

这样的测试方式对开发团队也提出了挑战,产品必须可已在在不同的平台下中通过命令行安装、启动、关闭、卸载,在linux的平台上这点非常容易做到,不管是aptitude还是pkg都通过命令行进行包管理,即便是通过shell启动也可以运行"KILL $PID"轻易的结束进程,而在WINDOWS平台就比较困难(但也是能做到的,只是比较丑陋),再就是如何确保测试可以在一个Repeatable的环境中运行,所谓Repeatable,是说对于每个测试运行前,产品可以被设置到确定的状态,当测试真实的产品时,这点是比较难于做到的,说到底是在两个进程间共享信息,能采取的手段比较有限(Open API + RESTFUL是个比较好的选择)

检验测试好坏的最好标准不是测试覆盖率,而是信心和安全感,如果你所在的团队对测试通过后发布软件信心满满,那么恭喜你,你们在作正确的事情。

2008年11月16日星期日

讲演培训中学到的

自信,从来都是说时有,用时无的。很少有人在开始讲演的时候就信心满满。大多数的情况下,讲演者都是从听众的反馈中不断积累信心的。这些反馈包括了卖关子时看见的期待眼神,听众赞同的点头,适时的提问等等。 有几个简单的实践可以帮讲演者源源不断的从观众中汲取信心。

* 合理的站姿,身体的姿态会在潜意识里影响观众对于演讲的印象,一个僵硬的站姿,会让人觉得压抑,抖腿,双手交叉等都是内心觉得不安全,想逃跑的表现。轻松站姿两年前就有人教过我,可惜境界一直没到,没领悟。 教我的人是我太极拳的师傅,老师说要“中正,安舒,沿路缠绵,静运无慌,肌肤骨节处处开张”,用在讲演中也是没错。

* 声音,洪亮的声音大概是抓观众注意力最简单的方法了,任谁也没法忽略台上站着的大喇叭。

* 适当的停顿,头至尾一直以相同的速度来进行,听众会被催眠的,譬如说我当年的政治老师就是此中高手。

* 目光接触,没有目光接触的观众会觉得被冷落,自然没兴致听下去。你也甭想得到任何正面的反馈。 如果台下都是dark look,就找几个脾气好的人目光接触吧。

* 诚实,其实是给自己减压的法门,讲演的时出错很正常,承认了,然后用备用方案就好了,没人会在意。越想掩盖,越多人会注意到。到时候大家就完全抱着看你出糗的态度了。当然如果你没准备备用方案,就不是诚实不诚实的事儿了,而是个大傻瓜。

2008年11月12日星期三

不同的思考方法

前几天产品发布,项目经理“Jez.谦虚” 同志要求我们审查一下2.0版本中有没有添加不能用于商业目的类库。因为类库很多,并且有很多的更新,删除,添加操作,很难一下找出到底在新版本中添加了那些文件。

于是我和哈达写了这样一个shell:

hg log -r 4281:tip --template '{node}\n' localivy |xargs -I % hg glog -p -r % | grep -A 1 '.*diff.*jar' | grep -B 1 'new file mode' > ~/Desktop/newlyAddedJars.diff

哈达和我都很欣赏使用shell来解决问题的方式, 所以写这段代码的时候,先想到了利用管道,以及grep过滤出所有在上次发布后和最新版本间添加的所有jar文件并把它们输出到文件中。

同时克里斯也在解决这个问题, 他把代码首先更新到上一个发布版本,然后利用meld将两个版本的ivy文件比较,人肉分析添加了哪些文件。 这个方案很简单,速度也很快。克里斯关注点在快速解决问题上,而不是“自动化的解决问题”


同组的李教授看到我们这么热闹,也饶有兴趣参与进来,他最近一直在研读mercurial宝典,对各种tip烂熟于心,他看到我们的解决方案后,写了另一段shell

hg log -r v1.0:tip --template '{file_adds} is added at revision {node}\n' localivy

这段shell充分利用了mercurial在template中定义的关键字做到了简单而强大,它可以打印出所有从上一个发布版本到最新版本间添加的文件以及相应的版本号。

解决这个问题的过程让我觉得很有兴趣, 同一个问题,由于解决者所关注的点以及知识域的不同出现了千差万别的解决方案。这些方案间的成本和收益也有着巨大的差别。

在日常工作中,我们都希望“简单而强大”的解决方案,单凭自身不断的宽展知识面是不够的,因为我们面临的问题多种多样,个人精力有限,个人的兴趣也限制了对于某些问题的深入研究,敏捷方法中的结对编程是一个很好地解决方案,更多拥有不同知识领域的人可以来共同解决一个问题,增加了我们找到“简单而强大”解决方案的几率,而开放空间可以让更多的人有机会参与到讨论中,贡献他们的聪明才智。爱凑热闹的李教授就是一例

2008年11月3日星期一

在Junit测试中使用 “前提”

在Cruise团队,QA经常会提交这样的bug, "某某功能在Windows上不工作 ",当然我并没有歧视Windows的意思,Windows在这句话里可以被替换为OSX, LINUX,乌龟SVN, Collable SVN,等等。在这样的团队中,面临着更多的不同平台(不仅仅是操作系统,还有不同发行版)带来的挑战。即便是Java这样号称“一次编写,随处运行”的语言,也必须常常去处理不同平台之间的些许差异。

作为一个TDDer,我们修复上述的bug的步骤是:

* 找到一台Windows机器
* 对上述Bug编写测试
* 运行测试得到“Red bar”
* 修改产品代码
* 运行测试得到"Green bar"


但是这样一个好的测试,却很可能无法提交,因为这特定平台的补丁可能会让别的测试失败。

这时候的无奈之选就是

@Test
public void featureShouldWorkOnWindows {
if (OSUtil.isWindows()) {
//Run the test.
}
}


这样,我们修复了bug,也编写了可靠的测试,除了测试代码有一点ugly. 这样的代码多了 ,也终究是一件恼火的事情。

前几天,写了一个对Junit的扩展,对于上面的测试,
可以这样来写
@RunWith(PrerequisiteAwareClassRunner.class)
public class TestCasesOnDifferentOS {
@Test
@Prerequisite(checker = OSChecker.class, arguments = OSChecker.LINUX)
public void shouldPassOnLinuxPlatform() throws Exception {
}
}

在这里,引入了前提条件@Prerequisite, 只有当前提满足的时候才会运行测试。

在项目里一个真实的例子是我们使用了ab(apache出品的性能测试工具,可以通过命令行调用)来进行性能测试,但并非所有的平台都能方便的安装ab。并且这个测试也无需在所有的平台上运行,通过使用前提,可以写下测试:
@RunWith(PrerequisiteAwareClassRunner.class)
public class TestCasesOnDifferentOS {
@Test
@Prerequisite(checker = AppsInstalledChecker.class, arguments = "ab -V")
public void shouldRunPerfWhenABIsInstalled() throws Exception {
}
}

在这里AppsInstalledChecker通过运行“ab -V”,并利用返回值判断是否当前平台上安装了ab,从而相应的运行或者忽略测试。


@Prerequisite中的Checker是一个接口
package com.googlecode.junit.ext;

public interface Checker {
boolean satisfy();
}
任何人都可以通过继承这个接口在扩展出适合应用场景的Checker。

@Prerequisite不是用于解决产品代码中的平台问题,作为junit的扩展,可以使用它让测试代码更干净,可读。

如果有兴趣,可以访问 http://code.google.com/p/junit-ext/来尝试一下。

Feedback或者Feature Request请发到:

iamkaihu@gmail.com
zee.ho.81@gmail.com

2008年10月20日星期一

带上用户的那顶帽子

你可曾在复杂的功能测试代码中挣扎着找出测试意图: 人肉过滤准备数据的过程. 抽丝剥茧的找到测试所覆盖的业务流程,猜测它究竟在测什么?

我们来看看这个测试:
userA logIn
rerun "defaultStage"
verify "defaultStage" is triggered and wait for completed
verify pipeline label of "defaultStage" is "1"
verify "secondStage" is triggered and wait for completed
verify pipeline Stage" is "1"

无可否认,测试达到了目的,验证了系统行为与期待一致。问题是这个测试中的“期待”到底是什么?

在带上用户的帽子回答这个问题的时候,我们很快意识到
verify "defaultStage" is triggered and wait for completed
verify pipeline label of "defaultStage" is "1"
对于用户来说意味着:
newly created "defaultStage" should be ran in the same pipeline

而后两句:
verify "secondStage" is triggered and wait for completed
verify pipeline Stage" is "1"
意味着:
next stage should be triggered automatically.

于是测试变成了:

userA logIn
rerun "defaultStage"
newly created "defaultStage" should be ran in the same pipeline
next stage should be triggered automatically.

测试变得更加可读,测试意图也变得更加明显。

作为Developer的特质之一就是细节驱动,我们将各种业务需求整理为更为详细的技术实现,并深陷其中, 造成了即便在完成功能测试的时候也不由自主的使用了经过“翻译”的语言。

特质之二就是记性不好(也许只是我),每天早上的Standup我都得靠着自己的Pair或者备忘录才能想起昨天作了什么。那么上面的测试失败时,我没信心自己会记得
verify "defaultStage" is triggered and wait for completed
verify pipeline label of "defaultStage" is "1"
到底意味着什么?

脱下Developer的帽子, 换一顶用户的帽子,来跟自己玩儿Q&A
"我要做什么?"
“login” 并 “rerun stage.”

"然后我必须要了解什么? 对我而言,什么是有价值的信息?"
“newly created "defaultStage" should be ran in the same pipeline”
“next stage should be triggered automatically.”

作为技术实现的:
verify "defaultStage" is triggered and wait for completed
verify pipeline label of "defaultStage" is "1"
是系统中的易变部分,随着系统的演化, pipeline label的格式很可能发生变化(譬如可订制),然而业务价值(newly created "defaultStage" should be ran in the same pipeline)却非常稳定,健忘如我也能立即捡起上下文。

带上用户的帽子,就是强迫自己以业务价值为角度进行思考。并将思考形式化,最终变成一种习惯。 而这种习惯将给团队带来更加简练,易读、易于维护的测试。

--
Hu Kai
blog : http://iamhukai.blogspot.com/

2008年10月15日星期三

使用Firebug的麻烦

firebug 的好处不多罗唆了,基本上可以满足开发,调试Web页面所有的需求。修改CSS, 强大的javascript调试功能, 便捷的页面Inspect,等等。

说下Firebug的Bug吧:

* 严重的内存泄露
大概是众所周知的麻烦了, 以至于当你打开gmail,它都会提醒你,firebug会让gmail变慢, bla bla bla。

会变慢多少呢?我和Tin同学很久以前写过一个小程序来测试自己的应用, 我们把每十秒一次的AJAX调用变为每一秒一次,在20分钟的时间内浏览器的内存占用从30M内存跑到900M。我们花了2天的时间来进行各个部分的javascript调优,大概让内存下降了几十M,对于整个内存泄露,简直就是杯水车薪,直到Tin同学无意间停掉firebug,同样的条件下,内存几乎没有增加。

* 关于缓存的假象
即便你正确的设定了Cache-Control, Expire等HTTP头, 你总会发现在被缓存的js, css文件出现在Firebug的Net视图中(被缓存的图片显示正常),并告诉你,下载这个文件花费了若干秒, 当你在无尽的Search中完全找不到浏览器拒绝进行缓存的头绪时。这里的讨论 会告诉你这是Firebug的一个Bug, WebKit似乎也有同样的问题。(今天和Tin同学用WiresharkLive HTTP Headers 验证了此问题,留此存照)




2008年10月9日星期四

使用Mercurial之乐

* 方便的安装。
不论是mac, linux还是windows,不论你是命令行的爱好者还是乌龟的忠实粉丝,你总能找到一款适合你的。

* 2个命令创建一个Mercuria仓库,
> hg init
> hg serve,
通过这两个命令你就可以拥有一个通过HTTP协议访问的mercurial仓库, 你可以方便的通过客户端通过命令访问,或者你可以轻松的使用浏览器来浏览当前的代码。

* 方便的分布式功能
上一次在印度我想在一台新电脑上安装源代码,无奈网络速度太慢,于是乎,我找到一个存有源码的机器,hg serve,这样我得到了一个本地服务器,通过它,我在1分钟内拿到了代码,然后将hgrc(一个mercurial的配置文件)的URL指向在中国的服务器,继续更新后面的几个patch。 将一个1个小时的操作变成2分钟的操作。

如果你急需要某个patch, 但是你的同事还没来得及提交到服务器上去,没关系,你大可以将自己的workingcopy指向同事的电脑, 运行hg pull就可以从他那里及时的拿到最新的代码。

没有branch的痛苦, 没有branch是因为每个人都是一个branch -_-!!!

* 便捷的本地提交
使用Mercurial,你可以在没有网络的情况下通过
> hg ci
进行本地提交,再也无需因为没有网络时候患上“写代码没有SCM恐惧症”,你也可以通过这个命令在日常开发中即达到小步前进,又不用每10分钟非得跑一遍测试。

* 离线操作
不论是Mercurial的提交或者是diff,rollback,strip, merge都可以在没有网络的情况下进行,想像一下在中国开发,服务器在美国的痛苦:那缓慢爬行的小乌龟。

* 速度优势
Mercurial是增量存储,并且它会每隔一段时间进行对整个Repository打一个快照,这样当你去clone repository(相当于svn checkout)的时候,它可以找到最近的一个快照,并在它的基础上应用后续的patch。

* 基于patch的管理
Mercurial将你的提交作为一个patch管理, 你可以很容易拿到别人的patch,通过hg客户端或者linux上的 patch命令将别人最新的修正打在你的工作目录里面。

* 更多的便捷操作
你想将本地的某些提交取消? hg strip
你想将server上的某些changeset取消?hg backout
你想订制hg log的输出方式?定义自己的hg template。

尝试Mercurial

2008年9月26日星期五

在Mac上安装Mercurial

Mercurial 是一种分布式版本工具, 它的本地提交, patch功能,以及对branch的管理非常优雅.

在ubuntu安装mercurial只需要运行sudo apt-get install mercurial

在Mac上安装Mercurial不是1-Click install

* 下载 http://mercurial.berkwood.com/
* 双击安装.
cat > ~/.profile
e
xport LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-s


* 打开hg view
hg clone http://selenic.com/hg hg-upstream
hg pull
hg update
sudo cp hg-upstream/contrib/hgk /usr/local/bin
cat > ~/.hgrc
输入

[extensions]
hgk=

[hgk]
path=/usr/bin/hgk
在 hg-upstream中运行 hg view, 应该可以看到hg view的UI




2008年7月27日星期日

在印度看蝙蝠侠

和D一起去看了蝙蝠侠,开着他老爹的现代, 正碰上印度的恐怖袭击, 车辆检查很严。看到一半的时候突然亮灯,人开始往外走, 原来是电影的中场休息。

看了一半的时候很想走,因为电影里太多的暴力,联想到印度的炸弹爆炸,很为电影院里的小朋友感到抱歉。大人们没有带给他们充满和平友爱的世界, 甚至在电影里也不曾带给他们。

当这世界全部的“希望”都只寄托在一个人的身上时,那么无论这个人是天使还是恶魔都无关紧要,因为这世界已陷入无底的深渊,再无其它可称之为“希望”的东西了。 蝙蝠侠就是这样一个怪物,用他自己的价值观“拯救世界”。

所有的暴力,请停止。

Cruise 发布前夜

周五在印度,和印度的同事一起准备Cruise的发布。没料想到的情况有:

License server所创建的evaulation license是一年有效的, 里面没有限制用户数量,事实上用户数量是没有设置的, 而cruise所期待的evaulatio license是一个月有效,6个用户。 在发布前两个小时一切就绪的情况下, 启动server输入evalution license, 我们得到的是NumberFormatException, 再修复了这个问题后发现,license server中key所对应的value是可以不存在的, 于是又有了NullPointerException。

由于对于License这部分的封装和测试做得比较好(不是Mock 测试, 而是实实在在的测试了解密,验证的整个过程), 我们很容易的用发生问题的license 重现了bug, 编写了两个测试,用了半小时左右来修复。 测试提交。

在就是修改license server, 使其符合cruise的需求, 大约也是半小时左右。

测试很重要,没有测试的覆盖,我和Chris无法从容的修改Cruise和License app, multi-skill非常重要,cruise用java编写,而license app用ruby rails编写。在这种情况下,以前在contention积累的rails经验帮了我。信任很重要,所有的人各司其职, 没有慌乱,也让我们减少了很多压力。

测试是我们的好朋友, 在这个重要的时候,是这些好朋友将我们从混乱中拯救了出来。

Cruise将按计划发布。

2008年6月30日星期一

让创意更有粘性





比起让所有人理解你试图表达的见解,图表。更重要的是讲一个成功的故事,让故事抓住你的观众,记住这个故事,也就记住了你, 记住了你的产品。

讲这样一个成功的故事,有六个秘诀, 我知道了,你呢?