RSpec的。在类方法测试中使用`double`

时间:2013-09-25 00:32:29

标签: ruby rspec

也许,会有太多代码,但我想理解为什么RSpec以这种方式工作。 我正在尝试double与远程REST API交互的外部对象。

示例代码:

class App

  class << self
    def foo
      external.func1
    end

    def bar
      external.func2
    end

    def external
      @external ||= ExternalObject.new
    end
  end

end

class ExternalObject
  def func1
    puts 'func1'
  end

  def func2
    puts 'func2'
  end
end

此规范有效:

require 'rspec'
require_relative 'app'

describe App do

  describe '.foo' do
    it 'calls func1' do
      expect_any_instance_of(ExternalObject).to receive(:func1)

      described_class.foo
    end
  end

  describe '.bar' do
    it 'calls func2' do
      expect_any_instance_of(ExternalObject).to receive(:func2)

      described_class.bar
    end
  end

end

但这不起作用:

require 'rspec'
require_relative 'app'

describe App do

  let(:external) { double('ExternalObject', func1: 1, func2: 2) }
  before(:each) do
    allow(ExternalObject).to receive(:new).and_return(external)
  end

  describe '.foo' do
    it 'calls func1' do
      expect(external).to receive(:func1)

      described_class.foo
    end
  end

  describe '.bar' do
    it 'calls func2' do
      expect(external).to receive(:func2)

      described_class.bar
    end
  end

end

当执行第二个示例时,RSpec输出看起来像double不再包含存根:

1) App.bar calls func2
     Failure/Error: described_class.bar
       Double "ExternalObject" received unexpected message :func2 with (no args)

如果将App.external中的代码替换为:

@external = ExternalObject.new
一切顺利。

我搜索了该错误的原因,但没有找到任何内容。

更新

另外,为了防止类的记忆,可以克隆对象。也许这不是一个好主意,但它确实有效。

require 'rspec'
require_relative 'app'

describe App do

  let(:external) { double('ExternalObject', func1: 1, func2: 2) }
  before(:each) do
    stub_const('App', App.clone)
    allow(ExternalObject).to receive(:new).and_return(external)
  end

  describe '.foo' do
    it 'calls func1' do
      expect(external).to receive(:func1)

      App.foo
    end
  end

  describe '.bar' do
    it 'calls func2' do
      expect(external).to receive(:func2)

      App.bar
    end
  end

end

更新2

我具体化了我的问题:为什么double@external之间在测试之间进行了记忆,但是没有存根方法?

1 个答案:

答案 0 :(得分:0)

因为let为每个示例创建了一个新的双精度。

尝试使用最新的(3.6)rspec运行此规范,您将收到以下错误:

1) App.bar calls func2
   Failure/Error: external.func2
     #<Double "ExternalObject"> was originally created in one example but has leaked into another example and can no longer be used. rspec-mocks' doubles are designed to only last for one example, and you need to create a new one in each example you wish to use it for.

这是因为在类级别上对象的记忆。

当第一个示例运行时,let会创建第一个具有第一个期望的double,并且会被记忆。

在下一个示例中,let创建一个新的double,并且期望值设置在该新的double上,但是当测试代码调用described_class.external时,它会收到第一个memoized test double而不是新的期望。

您可以尝试参阅以下示例:

RSpec.describe App do
  let(:external) { double('ExternalObject', func1: 1, func2: 2) }

  before { allow(ExternalObject).to receive(:new).and_return(external) }

  describe '.foo' do
    it 'calls func1' do
      p external.object_id # => 70184943614340
      p described_class.external.object_id # => 70184943614340
      expect(external).to receive(:func1)

      described_class.foo
    end
  end

  describe '.bar' do
    it 'calls func2' do
      p external.object_id # => 70184942937140
      p described_class.external.object_id # => 70184943614340
      expect(external).to receive(:func2)

      described_class.bar
    end
  end
end