Chapter 13 RSpec::Expectations

assertions OR expectations
BDD里, 使用expectations替代了传统测试里的assertions
虽然作用基本是一样的, 但是2者理念不同

传统测试我们先有了代码, 于是我们可以断言(assert)一段代码执行之后会出现生么状况
但是在BDD中, 测试之前还没有代码本体. 我们把自己化身为各种角色, 做出各种行为, 然后期待(expect)会得到某样结果

13.1 should, should_not, and matchers

RSpec为所有Ruby对象添加了should()should_not()方法
每个方法可以接受一个matcher对象或一个包含了特定范围内的ruby操作符的ruby表达式作参数

1
result.should equal(5)

这段代码会先对equal(5)求值, 这是一个RSpec提供的方法, 可以返回一个matcher对象,
之后这个matcherd对象被传给result.should.
should会调用matcher.matches?, 并把self(在这里即是result)作为参数.
如果matches?(self)返回true, 则expectations通过, 开始执行example里下一行代码,
否则should()方法会向matcher索取错误信息并报ExpectationNotMetError

13.2 Built-in Matchers

1
2
3
include(item)
respond_to(message)
raise_error(type)

相等

对于ruby中的四种相等, rspec提供了以下四种对应:

1
2
3
4
a.should == b
a.should === b
a.should eql(b)
a.should equal(b)

另外还要注意 不要使用 != , 而是要用RSpec里提供的should_not方法
因为==实际是调用的ruby方法, 于是 actual.should == expected会被解释称actual.shoul. ==(expected)

actual.should != expected会被解释成!(actual.should.==(expected))
actual和expected如果不等, 则会直接报ExpectationNotMetError

浮点数

1
result.should be_close(5.25, 0.005)

多行文本

1
2
result.should match(/this expression/)
result.should =~ /this expression/

变更

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
expect {
  User.create!(:role => "admin")
}.to change{ User.admins.count }

expect {
  User.create!(:role => "admin")
}.to change{ User.admins.count }.by(1)

expect {
  User.create!(:role => "admin")
}.to change{ User.admins.count }.to(1)

expect {
  User.create!(:role => "admin")
}.to change{ User.admins.count }.from(0).to(1)

expect {
  seller.accept Offer.new(250_000)
}.to change{agent.commission}.by(7_500)

最后面那个也可以写成

1
2
3
agent.commission.should == 0
seller.accept Offer.new(250_000)
agent.commission.should == 7_500

报错

1
2
3
4
5
6
expect {
  account.withdraw 75, :dollars
}.to raise_error(
  InsufficientFundsError,
  /attempted to withdraw 75 dollars/
)

raise_error可以传0个,1个或2个参数
第一个参数可以是error class, 错误信息的字符串 或者匹配错误信息的正则表达式
如果第一个参数为error class, 可以传第二个参数, String或Regexp

手动抛出异常

1
2
3
4
5
course = Course.new(:seats => 20)
  20.times { course.register Student.new }
lambda {
  course.register Student.new
}.should throw_symbol(:course_full, 20)

参数形式与raise_error类似, 但是第一个参数必须为symbol, 第二个参数可为任意类型

13.3 Predicate Matchers

所谓predicate method, 即是以?并且返回一个boolean值的方法
在RSpec里, 可以用be_xxx, be_a_xxx, be_an_xxx 来描述一个predicate method

13.4 Be True in the Eyes of Ruby

1
2
3
4
5
6
true.should be_true
0.should be_true
"this".should be_true

false.should be_false
nil.should be_false

对于特殊的只期待true/false的场合, 可以使用equal

1
2
true.should equal(true)
false.should equal(false)

13.5 Have Whatever You Like

have_xxx

has_xxx?这类predicate method, 可以使用have_xxx的Predicate Matchers

1
2
request_parameters.has_key?(:id).should == true
request_parameters.should have_key(:id)

Owned Collections

1
field.players.select {|p| p.team == home_team }.length.should == 9

够ruby, 但是不够English, 于是可以写成

1
home_team.should have(9).players_on(field)

其中, have()返回一个无法响应players_on()方法的matcher
之后这个matcher把players_on()方法代理到home_team

这么写一来易读(从English角度), 二来可以鼓励添加诸如players_on()这样的有用的方法

Unowned Collections

对于需要描述的对象本身就是collection的情况, 需要判断其size/length

1
collection.should have(37).items

其中items是语法糖, 后面会有进一步说明

同样的, String也适用, 其中characters也是语法糖

1
"this string".should have(11).characters

除了have()以外, 还有have_at_least()have_at_most()
have_exactly()have()同义

1
2
3
day.should have_exactly(24).hours
dozen_bagels.should have_at_least(12).bagels
internet.should have_at_most(2037).killer_social_networking_apps

have()究竟如何工作的

have()方法会返回一个RSpec::Matchers::Have实例
实例里面记录了传入have()的数目作为指定collection的包含元素数目的期待值

1
result.should have(3).things

其实等价于

1
result.should(Have.new(3).things)

Have类重写了method_missing方法, 使其能记录自己无法响应的方法(在这里就是things), 并返回self
于是Have.new(3).things, 最终返回了一个包含期待的collection元素数目(3)以及可能的collection名字(things)的Have对象

紧接着, 这个Have对象被传递给了should()方法
should()调用matcher.matches?(self)

而在matches?()方法里, 首先会判断目标对象(result)是否能响应之前纪录的things方法
若能相应, 则在result.things上调用length或者size(length优先)
此时如果result.things没有lengthsize, 就会得到一个error message 如果有length/size, 便会与Have对象里记录的数目作比较, 判断example通过或者失败

如果result无法响应things, 则会在result自身调用lengthsize
之后的判断与上面一样, 返回错误信息或者比较数目是否相等

13.6 Operator Expressions

1
2
3
4
5
6
result.should == 3
result.should =~ /some regexp/
result.should be < 7
result.should be <= 7
result.should be >= 7
result.should be > 7

这些会被Ruby解释成

1
2
3
4
5
6
result.should.==(3)
result.should.=~(/some regexp/)
result.should(be.<(7))
result.should(be.<=(7))
result.should(be.>=(7))
result.should(be.>(7))

RSpec在should()返回的对象上定义了==, =~, 在be()返回对象上定义了<, <=, >=, >

13.7 Generated Descriptions

1
2
3
4
5
6
7
8
9
describe "A new chess board" do
  before(:each) do
    @board = Chess::Board.new
  end

  it "should have 32 pieces" do
    @board.should have(32).pieces
  end
end

因为example运行时输出的内容几乎和example是一样的, 于是上述内容也可以写成

1
2
3
4
describe "A new chess board" do
  before(:each) { @board = Chess::Board.new }
  specify { @board.should have(32).pieces }
end

这2段代码输出的内容都是

1
2
A new chess board
  should have 32 pieces

specify()it()一样, 都是example()的方法别名

13.8 Subjectivity

subject()相当于在before里创建一个当前example的subject(被描述的对象)

1
2
3
4
describe Person do
  subject { Person.new(:birthdate => 19.years.ago) }
  specify { subject.should be_eligible_to_vote }
end

一旦subject被声明了, should()should_not()都会被代理到subject
于是上面的代码还可以写作

1
2
3
4
describe Person do
  subject { Person.new(:birthdate => 19.years.ago) }
  it { should be_eligible_to_vote }
end

如果创建的subject在执行new时不需要参数, subject的声明也可以不写, 直接写作

1
2
3
describe RSpecUser do
  it { should be_happy }
end

在ActiveAdmin下自定义filter

rails4之后,之前的`model`里加入`search_methods :filter_name`的做法已经失效,需修改为:``` ruby class PromoCode (ids) { with_tags(ids) } def self. …… Continue reading

Mac上搭建Phonegap环境

Published on October 02, 2014

Rails遗留程序里最常犯的错误(译)

Published on July 23, 2014