Rspec 中的 Should_receive


第一次在 Rspec 中使用 method mock 测试, 所以就碰到了坑. 前段时候学习了 Testing with Rspec 对 Rspec 入门. 现在真正使用起来, 还是会碰到很多小细节的问题, 例如今天碰到的这个: should_receive 所检查的对象.

两个概念

在课程的 mocking and stubbing 章节中有说明:

  • Stub: For replacing a method with code that returns a specified result.
  • Mock: A stub with an expectations that the method gets called.

在我自己的理解:

  • Stub: 是用来替换掉原来的方法, 并且返回一个指定的值. 他注重的是在测试某一个方法内部调用其他方法的时候, 能够省去考虑内部某一方法的实现细节, 转而将这个原始方法使用另外一个 stub 来替换掉他并且给与指定的值, 以测试当前需要测试的这个方法.
  • Mock: 首先一个 Mock 其实本身就是一个 stub, 不过还为其增加了对方法调用的期望测试. 他补充了普通 stub 会遗漏的一个点, 方法是否会被执行, 就好比当前测试的方法内部有一个 if 语句满足才会调用内部另外一个方法, 而测试需要确保这个方法是被调用了(如果带上没有返回值更好理解), 那么 stub 则无法确保这个测试, 而 Mock 则可以.

例子

这里有一个使用 Mock 的例子

1
2
3
4
5
6
7
8
it 'should not add to versions' do
  version = FactoryGirl.build(:version, created_at: Time.now - 20.hours, updated_at: Time.now - 20.hours)
  @listing.should_receive(:latest_version)

  expect {
    @listing.add_to_versions(version.attributes)
  }.to_not change { Version.count }
end

在这段代码中, 我希望测试一个名为 add_to_versions 的方法, 在这个 spec 中我希望测试的点有:

  1. 这个 spec 中的 version 传入 add_to_versions 经过计算后, 会舍弃掉这个 version
  2. 因为判断方法成功的标准和没有调用方法一样(Version.count 不变), 所以在 add_to_versions 的方法过程中, 我还需要判断其成功调用了 latest_version 确保是执行了对 version 的检查.

Mock

按照这样的目的, 所以我对第二点的测试需要使用 mock 方法, 我期望在测试 add_to_versions 方法调用后 Version 的总数量不会改变, 但是需要确认调用过 latest_version 方法进行过判断. 所以会拥有

1
@listing.should_receive(:latest_version)

加入 @listing 的 latest_version 没有被调用会抛出异常的(默认期望调用一次).对于默认情况的 mock 方法, 其实看看 rspec 对于 should_receive 的实现就能知道了(代码好绕 @,@), 他利用 alias_method 将原始方法改名藏起来了

instance_method_stasher.rb
1
2
3
4
5
6
7
8
9
10
def stash
  return if !method_defined_directly_on_klass? || @method_is_stashed

  @klass.__send__(:alias_method, stashed_method_name, @method)
  @method_is_stashed = true
end

def stashed_method_name
  "obfuscated_by_rspec_mocks__#{@method}"
end

最后这个 mock 方法就是一个 Rspec 的 MessageExpectation 对象, 并且被一个 MethodDouble 对象包含着, 同时 MethodDouble 又被一个 Proxy 包含着.

method_double.rb
1
2
3
4
5
6
7
8
9
10
11
12
def add_expectation(error_generator, expectation_ordering, expected_from, opts, &implementation)
  configure_method
  expectation = MessageExpectation.new(error_generator, expectation_ordering,
                                       expected_from, self, 1, opts, &implementation)
  expectations << expectation
  expectation
end

# 最后用来调用测试的入口
def verify
  expectations.each {|e| e.verify_messages_received}
end

也就是说, 从调用 @listing.should_receive(:latest_version) 后 Rspec 为我们做了:

  1. 为当前对象添加了一个 Rspec Proxy 代理 [methods.rb]
  2. 为当前对象与指定的方法包装在一个 MethodDouble 对象中 [proxy.rb]
  3. 根据后续的 and_return, at_least 等等为 MethodDouble 初始化一个 MessageExpectation (一个方法) 对象并增加你期望的方法的行为 [method_double.rb, message_expection.rb, instance_method_stasher.rb]

如果再 should_receive(:method_name) 那 Rspec 会重用 Proxy 与 MethodDouble, 但会拥有新的 MessageExpectation.

And_call_original

当我写完这个测试, 看着自己的 @listing.latest_version 的实现的时候发现, 如果我仅仅为 latest_version 增加一个 should_receive 那这个方法会拥有默认返回值为 nil, 那放到 add_to_versions 方法中, 那测试的不就不是我想要的逻辑了吗? 因为 latest_version 方法的返回值被我固定了啊? 可我期望的是能够正常执行 latest_version 找到最新的那个版本. 所以在 Rspec 官方找到了 Calling the original method , 同时也将测试代码进行了调整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 将这个方法放到一个 context 中
context '#add_to_versions' do
  # 对所需要的数据进行初始化
  before do
    @listing.save
    3.times do |i|
      offset = 24 - (i * 5)
      FactoryGirl.create(:version, listing: @listing, created_at: Time.now - offset.hours, updated_at: Time.now - offset.hours)
    end
  end

  # 最后来测试
  it 'should not add to versions' do
    version = FactoryGirl.build(:version, created_at: Time.now - 20.hours, updated_at: Time.now - 20.hours)

    # 确保执行了 latest_version message
    @listing.should_receive(:latest_version).at_least(:once).and_call_original
    expect {
      @listing.add_to_versions(version.attributes)
    }.to_not change { Version.count }
  end
end

看到调用了 and_call_original 我脑袋里面在想, 这个是怎么弄的? 一个标示符?然后带着疑问打开了源代码看到了

message_expectation.rb
1
2
3
4
5
6
7
def and_call_original
  if @method_double.object.is_a?(RSpec::Mocks::TestDouble)
    @error_generator.raise_only_valid_on_a_partial_mock(:and_call_original)
  else
    @implementation = @method_double.original_method
  end
end
method_double.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def original_method
  #here
  if @method_stasher.method_is_stashed?
    ::RSpec::Mocks.method_handle_for(@object, @method_stasher.stashed_method_name)
  elsif meth = original_unrecorded_any_instance_method
    meth
  else
    begin
      original_method_from_ancestor(object_singleton_class.ancestors)
    rescue NameError
      raise unless @object.respond_to?(:superclass)
      original_method_from_superclass
    end
  end
rescue NameError
  Proc.new do |*args, &block|
    @object.__send__(:method_missing, @method_name, *args, &block)
  end
end

这段代码比较多, 主要作用就是去寻找, 应该很多时候都会是进入 method is stashed 中的判断语句. 因为 add_expectation 中调用了 configure_method 同时这个方法就对需要测试的方法的原始方法进行了 stash.

这个测试方法写到这里, 也算 ok 完成了, 哎, 谁叫自己刚刚接触 Rspec 呢? 一个测试方法写这个长, 看了这么多的源代码还写了这么多字, 感慨, 写出一个好的测试用例也不容易啊.

在刚开始阅读 Rspec 的文档的时候是一头雾水, 不知道从哪个地方开始看起, 只好从 CodeSchool 或者其他的地方了解了基本使用, 再回过头来写测试的时候发现真正的问题的时候才知道该如何去查, 温故而知新 很有道理.

Feb 27th, 2013


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值