显示标签为“RSpec”的博文。显示所有博文
显示标签为“RSpec”的博文。显示所有博文

2008年11月11日星期二

2008年6月17日星期二

单独运行一个RSpec测试

spec spec/lib/xx_spec.rb -s 'description ...'

其中description ... 是describe 'xxx' do 里的xxx加上 it 'should xxx' do 里的字符串。

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也应该敏捷,与主题无关的废话尽量不说,此篇结束。

2008年1月8日星期二

Behaviour Driven Development Intro

看了Dave Astels写的《BDD Intro》,给了我很多观念上的改变。
  1. focus on testing? 原先的Test Driven Development(TDD),即测试驱动开发,很容易给人们以名称上的误解,以为就是先写测试。而实际上程序员写的测试和测试人员的测试还是有很大 的不同,程序员写测试是为了描述程序要实现的功能,从而来指导自己的设计与实现。所以用behaviour来进行描述,更加自然,更加贴近需求。
  2. 单元测试之误。Unit Test,很容易给人一种感觉,即测试方法与实现方法应该一一对应(1-1 relationship)。而BDD可以一次只描述行为的一个方面(we should be thinking about facets of behaviour),比如在某个上下文环境中,方法应该successful,在另一个上下文环境中,方法又应该failed。描述与实现的方法可以一对多,也就是说BDD的粒度比TDD要小。
  3. specifications VS Verification. 应该是说明(用should),而不是证实(用assert)。“说明”是要描述程序功能应该是什么样的,should_be xxx。“证实”是要确保程序正确运行,这里就有点奇怪,我还没有写程序,怎么知道程序运行正不正确呢?当问题比较简单的时候,开发者一般是没意识到的, 因为下意识里,在写测试的时候,已经在想程序的实现细节了。但是当问题比较复杂的时候,有些开发者就不知道该怎么写测试,或者写出的测试也很复杂,甚至写 出的测试本身都有问题。遇到这种情况,就应该把复杂的问题分解。而BDD就是在引导我们分解问题,通过一个个“在什么什么情况下,程序应该怎么怎么样”, 问题很自然很清晰地描述出来了。原先问题是立体的多面体,现在被展开铺平了。然后再把展开的facets放到一个特制的“点钞机”(autotest) 上,它会告诉你接下来该做什么。
  4. 很小,很精干。在TDD的指导下,通过重构,在大多数情况下我们已经可以让实现方法更短小,更简单,更易理解,但如果方法的分支多一点,异常情况多一 点,对应的测试方法往往会膨胀得“很好,很强大”(可能是功力不够,实现方法也没有重构得很彻底)。BDD本身就让我们每次描述专注在行为的某个方面上, 首先就保证了测试简单容易理解。当然在实际用RSpec的时候,往往受以前写单元测试的惯性思维,总想把功能在一个测试里都描述出来,结果有些测试还是写 得比较臃肿。用的是BDD的形,做的却是旧一套的事。看来工具只能起到有限的约束,只有真正经过实践积累,领悟到了工具的良苦用心,观念上转变了,再强迫 做法的改变,正确地用工具,才能事半功倍。
  5. expected, actual. 我以前在写测试的时候,总有某个时候会忘记,在assertEquals()方法的参数列表里,到底哪个排在前面,是expected还是actual? 虽然写反了并不影响功能,因为等式两边可以交换,等式仍然成立。但当测试出错时,打印的出错信息就会让人迷惑。而RSpec里,根本不用去想到底顺序该如 何,actual.should_equal expected,should后面就该接期望值,很自然的事。因为在使用自然语言时,这样的逻辑已经重复了无数遍,已经深入到了潜意识里,根本不用再思维。
  6. 作者在最后很有意思,既然TDD已经成那样了,人们已经理解错了也用错了,何必再费力气去纠正他们呢,干脆另起炉灶,重新树立一个品牌,就叫BDD。
  7. 最后,其实在项目中真正用的是RSpec + RSpec on Rails,RSpec是针对Ruby的,后者才是针对Rails的扩展。

感觉BDD与TDD并不是矛盾的,并不是说BDD就一定比TDD高级先进。TDD与BDD的目的是一样的,都是用来驱动程序的设计与实现的,用来确保程序 实现了预期的功能,它们提倡的小步迭代,更是在持续地纠正你的方向,让你不至于在错误的道路上越走越远。只是TDD被很多开发者误解,没有领会到它的精 髓,没有正确运用,没完全享受到TDD的好处。BDD进行了一些改进,在观念上强迫你按照behaviour的方式思维,在使用上更加接近于自然语言,让 开发者用编程语言写描述就像是在写一句话一样。
对于真正熟练正确运用的开发者,其实TDD与BDD也就是招式的不同。所谓高手,一草一木皆可为剑。

题外话:原来behaviour和behavior都是正确的拼写,类似于colour与color。