16

我有一个关于如何在示例之间共享 rspec-mocks 的问题。我正在用 rspec-mocks 编写一个新的 rails 应用程序3.1.3。我习惯于使用旧的 (< 2.14 并且如果当前的 rspec 使用情况,我会尝试更新我的知识。

我有一个模型方法:

def self.from_strava(activity_id, race_id, user)
  @client ||= Strava::Api::V3::Client.new(access_token: 'abc123')

  activity = @client.retrieve_an_activity(activity_id)

  result_details = {race_id: race_id, user: user}
  result_details[:duration] = activity['moving_time']
  result_details[:date] = Date.parse(activity['start_date'])
  result_details[:comment] = activity['description']
  result_details[:strava_url] = "http://www.strava.com/activities/#{activity_id}"


  Result.create!(result_details)
end

这是规格:

describe ".from_strava" do
  let(:user) { FactoryGirl.build(:user) }
  let(:client) { double(:client) }
  let(:json_response) { JSON.parse(File.read('spec/support/strava_response.json')) }

  before(:each) do
    allow(Strava::Api::V3::Client).to receive(:new) { client }
    allow(client).to receive(:retrieve_an_activity) { json_response }
    allow(Result).to receive(:create!)
  end

  it "sets the duration" do
    expect(Result).to receive(:create!).with(hash_including(duration: 3635))
    Result.from_strava('123', 456, user)
  end

  it "sets the date" do
    expect(Result).to receive(:create!).with(hash_including(date: Date.parse("2014-11-14")))
    Result.from_strava('123', 456, user)
  end
end

当我自己运行一个测试时它很好,但是当我运行整个describe ".from_strava"块时它会失败并显示消息

Double :client 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.

我明白它在说什么,但肯定这是对double2 个示例中使用的 a 的适当使用。毕竟,client双精度对示例并不重要,它只是我加载预设响应的一种方式。我想我可以使用 WebMock,但这似乎非常低级,并且不能很好地转化为实际编写的代码。毕竟,每个例子我们应该只断言一件事。

我曾想过client用一个电话来代替双重

allow(Strava::Api::V3::Client).to receive_message_chain(:new, :retrieve_an_activity) { json_response }

但这似乎也不是正确的方法,因为文档指出这 receive_message_chain应该是代码气味。

因此,如果我不应该使用receive_message_chain, shared clientdouble 并且还遵循标准 DRY 原则,那么我应该如何解决这个问题?

我希望对此有一些反馈。

谢谢,戴夫

4

4 回答 4

16

我不喜欢这个公认的答案。通常确实需要为外部组件缓存客户端(保持活动连接/您可能需要的任何 SSL 设置等),并且为了解决测试问题而将其删除并不是一个理想的解决方案。

为了修复您的测试(不重构代码),您可以执行以下操作以在每次测试后清除实例变量:

after { Result.instance_variable_set("@client", nil) }

虽然不可否认,这不是最干净的解决方案,但它似乎是最简单的并且实现了两者,让您有一个清晰的设置,在测试之间没有共享状态,并让您的客户端缓存在“正常”操作模式下。

于 2018-05-11T13:36:16.260 回答
8

当然,这是在 2 个示例中使用的双精度的适当用法。

不,这不对。:) 您正在尝试使用类变量;不要这样做,因为变量不跨越示例。解决方案是每次都设置客户端,即在每个示例中。

坏的:

@client ||= Strava::Api::V3::Client.new(access_token: 'abc123')

好的:

@client = Strava::Api::V3::Client.new(access_token: 'abc123')
于 2014-11-23T21:15:19.200 回答
3

我在我的一个应用程序中有相同的用例,我们通过将缓存提取到私有方法中然后存根该方法以返回双精度来解决它(而不是new直接存根方法)。

例如,在被测类中:

def self.from_strava(activity_id, race_id, user)
  activity = strava_client.retrieve_an_activity(activity_id)
  ...
end

private
def self.strava_client
  @client ||= Strava::Api::V3::Client.new(access_token: 'abc123')
end

在规范中:

let(:client) { double(:client) }

before { allow(described_class).to receive(:strava_client).and_return(client) }

...
于 2015-02-25T21:08:33.337 回答
2

TLDR:添加after { order.vendor_service = nil }以平衡before块。或者继续阅读...

我遇到了这个,它来自哪里并不明显。在 order_spec.rb 模型测试中,我有这个:

  describe 'order history' do
    before do
      service = double('VendorAPI')
      allow(service).to receive(:order_count).and_return(5)
      order.vendor_service = service
    end   

    # tests here  ..
  end

在我的Order模型中:

  def too_many_orders?
    @@vendor_service ||= VendorAPI.new(key: 'abc', account: '123')
    return @@vendor_service.order_count > 10
  end

当我只在 order_spec.rb 上运行 rspec 时,这很好用

我在 order_controller_spec.rb 中模拟了一些完全不同的东西,有点不同,使用allow_any_instance_of()代替doubleand allow

  allow_any_instance_of(Order).to receive(:too_many_orders?).and_return(true)

这也很好地测试了。

令人困惑的问题是,当我运行全套测试时,我在控制器模拟上得到了 OP 的错误——使用allow_any_instance. 这很难追查,因为问题(或至少我的解决方案)在于我使用的模型测试中double/allow

为了解决这个问题,我添加了一个after块来清除类变量@@vendor_service,平衡before块的动作:

  describe 'order history' do
    before do
      service = double('VendorAPI')
      allow(service).to receive(:order_count).and_return(5)
      order.vendor_service = service
    end   

    after do
      order.vendor_service = nil 
    end

    # tests here  ..
  end

这迫使在以后的不相关测试||= VendorAPI.new()中使用真正的new功能,而不是模拟对象。

于 2015-11-04T02:25:58.163 回答