rspec-mocks' doubles are designed to only last for one example

daveharris picture daveharris · Nov 23, 2014 · Viewed 8.2k times · Source

I've got a question about how to share rspec-mocks' double between examples. I'm writing a new rails app with rspec-mocks 3.1.3. I'm used to using the old (< 2.14 and and trying to update my knowledge if current rspec usage.

I have a model method:

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

And here is the spec:

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

When I run a single test on it's own it's fine, but when I run the whole describe ".from_strava" block it fails with the message

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.

I understand what it's saying, but surely this is an appropriate use of a double being used in 2 examples. After all, the client double isn't important to the example, it's just a way for me to load the canned response. I guess I could use WebMock but that seems very low-level and doesn't translate well to the actual code written. We should only be asserting one thing per example after all.

I had thought about replacing the client double with a call to

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

but that doesn't seem to be the right approach either, given that the documentation states that receive_message_chain should be a code smell.

So if I shouldn't use receive_message_chain, shared client double and also follow the standard DRY principle then how should I fix this?

I would love some feedback on this.

Thanks, Dave

Answer

Marcin Kołodziej picture Marcin Kołodziej · May 11, 2018

I'm not a fan of the accepted answer for this one. Caching clients for external components can often be really desired (keeping alive connections/any SSL setup that you might need, etc.) and removing that for the sake of fixing an issue with tests is not a desirable solution.

In order to fix your test (without refactoring your code), you can do the following to clear the instance variable after each of your tests:

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

While admittedly, this is not the cleanest solution, it seems to be the simplest and achieves both, lets you have a clear setup with no state shared in between tests, and keep your client cached in "normal" operation mode.