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同学用Wireshark 和 Live 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将按计划发布。