我是一个Test,准确地说是一个Rails Unit Test。我的名字有点长,据说是为了清楚地说明我要承担的职责和任务,废话就不多说了,大家直接看我的code吧:
=============== ruby code ================
def test_should_compare_user_name_first_when_compare_two_user
user1 = User.new(:name => ‘name1’)
user2 = User.new(:name => ‘name2’)
assert_equal -1, user1<=>user2
end
=======================================
“坐家”创造了我,他今天似乎心情不错,和一个刚毕业的小伙“配儿”,好像叫“张三石”。很显然,我是“坐家”to-do-list上的一个小task,他的短期目标很可能是对User对象数组按照需求进行排序。“坐家”一如既往地急性子,不等他的“配儿”反应就很快地运行了一下我,然后就在User类中输入了一段Fake代码:
=============== ruby code ================
def <=> other
-1
end
=======================================
显然,我不可能失败,虽然我很想。“这样也行?”我觉得有点不爽,因为“坐家”显然是写了一段敷衍我的代码。张三石见到成功的绿条显得蠢蠢欲动,趁“坐家”端起他的星座杯压口茶之际不甘示弱地抢过键盘开始敲下一个Test的代码。“这个毛毛草草的家伙,居然留了那样的代码就不理我了,那要我干什么?”我心里蛮不服气的,有被欺骗的感觉,撇了撇刚刚诞生的Test:
=============== ruby code ================
def test_should_compare_age_when_they_have_same_name
age1 = 1
age2 = 2
name = 'name'
user1 = User.new(:name => name, :age => 1)
user2 = User.new(:name => name, :age => 2)
assert_equal age1
<=> age2, user1
<=> user2
end
=======================================
“坐家”在等张三石敲完所有代码之后,皱了皱眉头,说道:“这是我们的to-do-list上第二个task所对应的Test,但是刚刚的代码只是个Fake,不能算完成第一个task的,我们应该先把第一个task做完。”
“可是第一个Test已经运行成功了呀,我想加下一个Test,运行失败后再改代码,这是标准的三角法。”
“嘿嘿……”从显示器后面传来一声淫笑,“华丽的三角法……”我看不到说话的人是谁。“八叉,又见八叉。”IntelliJ似乎看透了我的心思,说道。IntelliJ的全名叫“IntelliJ IDEA”,据说是很专业的Java语言IDE工具,“坐家”的最爱,因此即使是做Rails开发,也非要用它。IntelliJ说“坐家”花了很多心思去找回来一个还没有发布的IntelliJ Rails开发Plugin,虽然很多地方还难以令人满意,但是能用它做Rails开发已经让“坐家”很开心了。
“哦。”我的脑子里浮现出一个脑袋,上面长着八把叉子,“一定是个怪人。”
“差不多。每次说到三角法,这家伙就免不了要发出几声淫笑。这次‘坐家’似乎又要较真了。”IntelliJ长叹了一声。
“你为什么说又?”我好奇地问。
“显然,这不是第一次在这个小问题上较真了。以前最严重的一次两个人吵了两个多小时,什么事也干不成,还搞得我们大家不能正常下班。”
“哦。”
“其实《Test-Driven Development by Example》一书上有一整章的内容讨论这个问题。可惜,‘坐家’看完之后就给忘了,再也从书上找不到了。所以每次他苦口婆心地想改掉别人的坏习惯都不太成功。加上他心急和口不择言,完全不能把问题给别人说清楚。”
“书上有的话,仔细翻翻还是容易找得到的吧。”
“嘿嘿,这家伙E文有限,所以浏览完全不起作用,光用眼睛是找不到他想搜索的内容的,所以他必须完整地重新读一遍才行。可是他又没有耐心去重新读,这家伙最缺耐心了。”
“哦,三角法,那么让我们来看看这华丽的三角法吧。”说着,“坐家”按下了运行测试的快捷键。
绿条!张三石挠了挠头,马上意识到了问题,迅速修正了测试代码:
=============== ruby code ================
def test_should_compare_age_when_they_have_same_name
age1 = 1
age2 = 2
name = ‘name’
user1 = User.new(:name => name, :age => age1)
user2 = User.new(:name => name, :age => age2)
assert_equal
age2
<=>
age1, user2 <=>
user1
end
=======================================
这下如愿以偿了。
“为什么这样改?在逻辑上来说,这和改之前是没有任何区别的。”“坐家”显然是明知故问。
“因为那样不能让它运行失败,一个运行失败的Test是我修改代码的主要原因。”
“哦,你是说没有运行失败的Test,就不改代码?”
“当然!如果所有的Test都是运行成功的,那么我为什么需要改代码?我们的Test不正是我们的需求吗?要是代码满足了我们的需求,那当然是不需要改的。”张三石确信自己把握了真理,中气十足。
“重构另当别论。”张三石又迅速补充了一句。
“这么说来,你写Test的目的是描述需求了?”
“恩。主要目的之一。”
“可是你的代码并不是按照Test的需求写的呀。第一个Test说,我们要按照用户名做比较,并因此给定了两个不同用户名‘name1’和‘name2’,而我们之所以写下这句‘assert_equal -1, user1
ó user2’,是因为我们的需求指定这两个给定的user对象根据其name值比较的结果是-1。”
“Test运行通过了呀。Test运行通过不正表明我们的代码在按照需求工作吗?”
“这么说,你认为评判代码是否按照需求设计的标准是Test运行通过?”
“当然!虽然现在的结果似乎并不是我们想要的,但是随着后续的Test不断被添加进来,我们可以让它变得完美。无论如何,如果你觉得代码有问题,那么就加一个运行失败的Test,然后修改代码让它运行通过。”
“好吧。你刚才把这个方法叫什么来着?”
“三角法。这个在《Test-Driven Development by Example》一书中就是这么介绍的。”张三石已经明显觉得“坐家”要挑刺儿,搬出了书压阵。
“哦,我看不见得,”“坐家”轻咳了一声,压了一口茶继续说道,“三角法是一种通过多个测试用例验证一个算法抽象的测试驱动开发策略。正常情况下,如非必要不应该采用。我以前说过,做测试驱动开发越是小步骤越是安全和可靠,所以你首先要养成小步骤进行测试驱动开发的习惯。这里说的小步骤,你可以理解为我们的to-do-list中的一个task,像我们今天这样的to-do-list,每个task实际上已经足够小了,基本上每个测试都有显而易见的答案摆在那里。而三角法,是在完成一个task的过程中,通过创建多个测试用例继续将这个task分解,最终达到抽象出合理算法并完成task的目的;或者是通过创建多个测试用例来确保当前的算法实现是符合task要求的。”
“需要说明的是,测试用例和我们写的一个Test不是同一个概念。一般情况下我们写的一个Test是一个测试用例,但是我们也可以在我们的一个Test中包含多个测试用例,如果它们都很简单。”
张三石显得有点不服气,想争辩点什么,但是马上被“坐家”示意制止了。
“不用担心,我所说的这些内容,稍后都会在《Test-Driven Development by Example》一书中给你指出来,它的作者说得很明白的,只是你没有留意罢了。”
“之所以讨论这个问题,我是想给你说,无论是三角法还是测试驱动开发本身,Test存在的目的,除了你所说的描述需求之外,还有更深层次的原因。”
“测试驱动开发的本质其实是人类自古以来解决复杂问题所普遍采用的方法:分而治之。因此,一个Test存在的目的不仅是描述需求并验证代码是否满足需求,而且要验证当代码按照我们预想的设计写出来时,它是满足需求的。也就是说,当我们把我们今天要达到的目标分解成一个个的task并写在to-do-list上之后,针对每个task我们会有一个解决它的想法或者说是设计,而我们写下Test的最主要的目的,应该是验证我们的解决这个小task的想法或设计是符合需求的。”
“看看我们的现在的两个Test,它们说明了两件事情:第一,比较两个User对象时,其结果是User对象的name属性比较的结果;第二,如果两个User对象name相同时,其结果是User对象的age属性比较的结果。我们本来将这两件事情分成两个task是将一个相对复杂的问题分解成两个相对简单的问题解决,可是现在的结果呢?由于采用Fake代码让第一个Test顺利通过,我们分解问题的努力付诸东流。当你写完第二个Test之后,你所面临的问题完全没有改变,你现在需要同时解决两个问题,或者你再次采用Fake代码的策略让Test通过,让下一个Test来解决。也就是说,你并没有通过测试驱动开发方法达到对复杂问题分而治之的目的。”
“有句话,出来混,迟早要还的。第一个Test运行通过,但没解决的问题,你迟早还是要解决它的。”
“可是,三角法……”张三石疑惑了。
“嗯,对不起,刚说着说着给跑远了,忘了先讨论清楚华丽的三角法了。”
“相对你的第二个Test,我觉得在第一个Test的最后加上这样的代码……”“坐家”一边说一边飞快地敲键盘。
=============== ruby code ================
def test_should_compare_user_name_first_when_compare_two_user
user1 = User.new(:name => ‘name1’)
user2 = User.new(:name => ‘name2’)
assert_equal -1, user1 <=> user2
assert_equal 1, user2
<=>
user1
end
=======================================
“更像是在应用三角法。因为它的出现,让我们把注意力继续放在没有解决的问题一上。这种方式正是我之前说的增加测试用例。我相信,你一定认为它看起来显得没什么存在的必要,而这恰恰就是在现在这种情况下不应用三角法的主要原因。所以,不要通过解决你的to-do-list上的下一个task来实现眼前的task所应该具备的逻辑,那样不是三角法。华丽的三角法通过增加测试用例来解决问题,或者是消除你对实现代码的顾虑。”
“但这是个小问题,即使象你所的,我把两个小问题合到一起去解决,我也能很快写出实现代码让它们通过。”在我看来,张三石的争辩苍白无力。显然,他已经被说服了,在找台阶下。
“嗯,确实是这样。所以,我并不介意你现在使用这种方式解决眼前的问题。只是提醒你,华丽的三角法其实并不是你想象的那样能让你更容易地解决问题,它甚至都不是你现在想象的那个样子。象你这种用法,很可能会让简单问题复杂化。至少,这样做不是个好的习惯。”
“好吧,我知道我很难让你服气,毕竟眼前的小问题似乎不值得这样小题大做。如果你有原版《Test-Driven Development by Example》一书,那么请翻到第151页,‘第28章Green Bar Pattern’。这章会讨论四个模式,其中的‘Traingulate’阐述了华丽的三角法以及你该在何时使用它,其中最后一段作者语重心长地推荐你不要优先使用这种策略。而‘Obvious Implementation’则讨论了该如何作出简单实现的选择。另外,这章的‘Fake It’模式也很值得一读,它可以帮助你搞清楚Fake代码和三角法之间的细微差别。”
“读完这些内容之后,我建议你再考虑一下每个Test存在的意义和目的,以及如何让测试驱动开发帮助你更好地解决问题。现在,让我们先快点搞定眼前的to-do-list先。”
“我不得不说,这小子现在有点上道了。”IntelliJ语重心长地说。
很快地,我被check in到codebase中去了。在之后的日子里,我被日复一日反复地运行,虽然很少人再看我一眼,但是我知道那只是因为他们相信我很好地完成了我的职责,相信我会在第一时间通知他们如果他们意外地改变了我所负责的那段简单的业务逻辑。在“坐家”和张三石“配儿”的过程中,我了解到了我存在的意义,这让我对千篇一律的生活充满了责任感,并因此认识了很多新朋友,像rake和CC大叔,它们经常会指挥我们运行,并报告我们的运行结果。搞笑的CC Monitor大哥会在某个测试运行失败的时候放出一段噪音。爱丽丝姐姐经常对这帮号称“敏捷”的家伙们把她弄脏耿耿于怀,我知道,那一定是有原因的,等她了解到,这些人如何把她改变成了一块非比寻常的玻璃墙之后,她也会对她所承担的职责感到骄傲。