RSpec, Test Double, Mock, and Stub

Rspec是ruby的测试框架之一。

Mockstub都属于Test double,用于测试时,模拟特定的方法或者对象的值或行为。

在Rspec中提供了这3种方法(gem rspec-mocks)。

Test Double


Test Double是一个模拟对象,在代码中模拟系统的另一个对象,方便测试。

例如我们有个智能书架(Bookshelf),可以自动统计书(book)的总重量(weight_of_books)。在测试这个方法的时候,需要我们有book这个对象,而Book类仍在开发中,不能被我们使用。

book = double("Book")

这样我们就有了一个book对象. 但现在book没有任何方法,这是空的类,我们可以使用stub等方法为他创建假方法。

同时,也可以直接创建一个有属性的book对象。

book = double("Book", weight: 80)

book1 = double("Book", weight: 80)
book2 = double("Book", weight: 20) 


bookshelf = Bookshelf.new

bookshelf.add(book1)
expect(bookshelf.weight_of_books).to eq(80)

bookshelf.add(book2)
expect(bookshelf.weight_of_books).to eq(100)

这样,我们就测试了bookshelf的方法。

instance_double 是RSpec 3之后替代double,并支持verifying double,意思是当使用stub的方法后,RSpec还会去验证被double的真实对象是否具有这个方法。

Method Stubs


Stub 既可以模拟假对象(test double)的方法,也可以模拟真对象(real object)的方法。RSpec-mock提供下面3种创建method stub的方法。


allow(book).to receive(:title) { "The RSpec Book" } # 第一个参数(book)是对象,第二个参数是方法, 第三个参数是方法的返回值
allow(book).to receive(:title).and_return("The RSpec Book")
allow(book).to receive_messages(
    :title => "The RSpec Book",
    :subtitle => "Behaviour-Driven Development with RSpec, Cucumber, and Friends")

上面等同于:

book = double("book", :title => "The RSpec Book" )

stub返回多个值

allow(die).to receive(:roll).and_return(1, 2, 3)
die.roll # => 1
die.roll # => 2
die.roll # => 3
die.roll # => 3
die.roll # => 3

每次调用stub的方法,会按顺序依次返回。

Mock


Mock与Stub的区别在于, Mock是对象层面的,Stub是方法层面。Mock对象同时支持method的stub和 消息验证(message expectation)

通常Rspec使用expect去验证message是否工作。

validator = double("validator")
expect(validator).to receive(:validate) { "02134" }
zipcode = Zipcode.new("02134", validator)
zipcode.valid?

如果validate消息在用例执行到最后有被接收到,则验证成功,否则失败。

新老版本差异


Rspec 老新语法差异(前为老,后为新),前后的作用基本一样:

  • stub vs allow
  • should_recevice vs expect().to recevice
  • any_instance (已遗弃)
  • stub_chain (已遗弃)
  • unstub vs and_call_original

1. stub vs allow

对于should 方法做断言,通常对应的stub方法:

describe "a stubbed implementation" do
  it "works" do
    object = Object.new
    object.stub(:foo) do |arg|
      if arg == :this
        "got this"
      elsif arg == :that
        "got that"
      end
    end

    object.foo(:this).should eq("got this")
    object.foo(:that).should eq("got that")
  end
end

对于RSpec3的 expect方法,对应的stub方法:

describe "a stubbed implementation" do
  it "works" do
    object = Object.new
    allow(object).to receive(:foo) do |arg|
      if arg == :this
        "got this"
      elsif arg == :that
        "got that"
      end
    end

    expect(object.foo(:this)).to eq("got this")
    expect(object.foo(:that)).to eq("got that")
  end
end

2. should_recevice vs expect().to recevice

老版本的Rspec(2.14), 使用should_receive. RSpec3使用了expect

Session.should_receive(:get).with('test_session_id').and_return(mock_session)
expect(Session).to receive(:get).with('test_session_id').and_return(mock_session)

3. any_instance (已遗弃)

Incident.any_instance.stub(:field_definitions).and_return([])

意思是Incident类的所有实例都具备stub的方法,不过该方法已经被遗弃。

RSpec.describe "Expecting a message on any instance of a class" do
  before do
    Object.any_instance.should_receive(:foo)
  end

  it "passes when an instance receives the message" do
    Object.new.foo
  end

  it "fails when no instance receives the message" do
    Object.new.to_s
  end
end

上述结果:

2 examples, 1 failure
Exactly one instance should have received the following message(s) but didn't: foo

任意new的Object类的实例都会有foo方法,当使用should_recevice时,所有的Object的实例都必须响应foo方法,否则就会报错。

stub_chain (已遗弃)

支持链式方法的stub,现在已经不推荐使用。

  example "using a string and a block" do
    dbl.stub_chain("foo.bar") { :baz }
    expect(dbl.foo.bar).to eq(:baz)
  end

unstub vs and_call_original

unstub将之前实例stub的方法disable掉,声明之后,stub的方法,将被原有的方法替代或者不存在。

RSpec.describe "Unstubbing a method" do
  it "restores the original behavior" do
    string = "hello world"
    string.stub(:reverse) { "hello dlrow" }

    expect {
      string.unstub(:reverse)
    }.to change { string.reverse }.from("hello dlrow").to("dlrow olleh")
  end
end

and_call_original 是类似的功能,使用with可以让特定的参数输入时,仍然适用stub的方法返回值

require 'calculator'

RSpec.describe "and_call_original" do
  it "can be overriden for specific arguments using #with" do
    allow(Calculator).to receive(:add).and_call_original
    allow(Calculator).to receive(:add).with(2, 3).and_return(-5)

    expect(Calculator.add(2, 2)).to eq(4)
    expect(Calculator.add(2, 3)).to eq(-5)
  end
end

配置返回方式

以下是 RSpec 3.4 版本特性

  • and_return 返回值
  • and_raise 抛出异常
  • and_throw 抛出symbol
  • and_yield 让方法接受块参数
  • and_call_original 调用原有的方法
  • and_wrap_original 使用块修改原有的方法

具体见: Configuring responses

References

instance_double and verifying double

rspec-verifying-doubles

rspec mock 2.14

Test double

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容