2008年1月9日星期三

An Introduction to RSpec

找到一篇文章,叫《RSpec简明指南》:http://blog.csdn.net/xieqibao/archive/2007/10/09/1816839.aspx
原文:http://blog.davidchelimsky.net/articles/2007/05/14/an-introduction-to-rspec-part-i

我在他的Blog里找了半天,也没找到传说中的part II,写技术文章也兴挖坑啊?

文章非常通俗易懂,举的例子也很简单。基本还是他和Dave Astels在RubyConf 2007上现场演示的内容。他哥俩现场表演pair programming,一个描述,一个实现,“很好,很敏捷”。
只是最后我觉得代码有点小问题:
class User
def in_role?(role)
role == "assigned role"
end
def assign_role(role)
end
end
这样居然作者就说一切都搞定了,很明显assign_role方法什么也没做过,而且从这步直接重构就跳到了:
class User
def in_role?(role)
role == @role
end
def assign_role(role)
@role = role
end
end
感觉逻辑上有断层。记忆中他们在RubyConf上演示的demo逻辑很完整,跟这个稍有不同。

于是把视频上的演示代码手工敲了下来:
describe User do
it "should be in a role to which it is assigned" do
dave.assign_role("speaker")
dave.should be_in_role("speaker")
end
end
注意,这里只是描述了dave的某个行为,根本没有管究竟有没有dave这个object,这是后面的事。
然后运行测试,当然会失败,在写实现代码以前,我们应该期望失败,然后实现功能,使其通过。

"undefined local variable or method 'dave' ... "

这里就是BDD的精髓之一了,描述完了后,运行测试,系统自然会告诉你,接下来应该做什么。

系统说需要一个名为dave的局部变量,那我们就照做:
describe User do
it "should be in a role to which it is assigned" do
dave = User.new
dave.assign_role("speaker")
dave.should be_in_role("speaker")
end
end
这次的失败信息不同了:

"undefined method 'assign_role' ... "


那么就来实现这个assign_role方法:
class User
def assign_role
end
end
失败信息:

"wrong number of arguments (1 for 0)"

哈哈,简直“懒”到了极致,你系统说没有方法,那我就给你个空的方法,任何多余的事我都不做。其实是“简”到了极致,跟描述无关的功能,我为什么要去实现呢?

class User
def assign_role(role)
end
end
失败信息:

"undefined method 'in_role?' ... "


class User
def assign_role(role)
end
def in_role?
end
end
失败信息:

"wrong number of arguments (1 for 0)"

这种迭代的步伐也小到了极致。

class User
def assign_role(role)
end
def in_role?(role)
end
end
终于迎来跟描述相关的失败信息了:

"expected in_role?("speaker") to return true, got nil"

dave说通常这时我们会用个非常简单,甚至有点愚蠢的做法来让测试通过。
david在旁边赞同:确实很愚蠢,但这就是我们的做法。

class User
def assign_role(role)
end
def in_role?(role)
true
end
end
终于通过了:

"1 example, 0 failures"

到目前为止,我们都没看过最初定义的行为描述,而是一直照着系统告诉我们的信息在做。系统就像一个不知疲倦的“客户”,当它把需求“背下来”后(我们描述完行为后),它就会不断地告诉你接下来该做什么,以及你刚才做的是否符合它的要求。

既然测试已经通过了,就没有必要再理实现代码了。尽管刚才我们用了个非常蠢的办法来使其通过,但它毕竟通过了。它又蠢又能通过,证明是行为描述得不全面,这是描述的问题,而非实现的问题(因为我们确实已经实现了描述所期望的功能,多余的任何一点事都不该做)。

接下来就是继续描述了,好让刚才那个蠢程序员继续有事可做:
describe User do
it "should be in a role to which it is assigned" do
dave = User.new
dave.assign_role("speaker")
dave.should be_in_role("speaker")
end

it "should not be in a role to which it was not assigned" do
aslak = User.new
aslak.should_not be_in_role("speaker")
end
end
理所当然地又失败了(这里如果没有失败的话,理论上是有问题的,因为客户新提的需求,程序没做任何改动居然就能满足了,要么是以前程序员太多事,自作主张加了些功能,要么是需求提得不对,跟以前的需求重复了):

"expected in_role?("speaker") to return false, got true"

哈哈,报应来了吧,叫你蠢,直接硬编码true,现在客户要false了。

现在就是发挥程序员作用的时候了,来真正意义上实现功能。刚才定义的两个方法,一个是空的,一个直接返回ture,都没做实质性的事。
class User
def assign_role(role)
@role = role
end
def in_role?(role)
@role == role
end
end
又通过了:

"2 examples, 0 failures"

这次我们没有做任何蠢事,我们真正实现了功能,同时满足了那两个描述。

这个demo实在是太棒了!

写Blog也应该敏捷,与主题无关的废话尽量不说,此篇结束。

没有评论: