Mike Blyth
Mike Blyth

Reputation: 4268

Can RSpec stubbed method return different values in sequence?

I have a model Family with a method location which merges the location outputs of other objects, Members. (Members are associated with families, but that's not important here.)

For example, given

Family.location might return 'San Diego (member_1 traveling, returns 15 May)' The specifics are unimportant.

To simplify the testing of Family.location, I want to stub Member.location. However, I need it to return two different (specified) values as in the example above. Ideally, these would be based on an attribute of member, but simply returning different values in a sequence would be OK. Is there a way to do this in RSpec?

It's possible to override the Member.location method within each test example, such as

it "when residence is the same" do 
  class Member
    def location
      return {:residence=>'Home', :work=>'his_work'} if self.male?
      return {:residence=>'Home', :work=>'her_work'}
    end
  end
  @family.location[:residence].should == 'Home'
end

but I doubt this is good practice. In any case, when RSpec is running a series of examples it doesn't restore the original class, so this kind of override "poisons" subsequent examples.

So, is there a way to have a stubbed method return different, specified values on each call?

Upvotes: 106

Views: 59982

Answers (7)

Constantin De La Roche
Constantin De La Roche

Reputation: 3320

A pattern that can help:

allow(MyClass).to receive(:my_method) do |kwargs|
    # define the response for "my_method" (kwargs is the hash of arguments given to the method)
    "my return value"
end

Upvotes: 0

CamiEQ
CamiEQ

Reputation: 771

I had the problem that I had multiple calls to a method and not always in the same order. In that case, I'd recommend using .with, to distinguish between instances, using the method's arguments.

So for example, this could be your "default" returning value:

allow(@family).to receive(:location).and_return('her_work')

but then, if location receives an argument like "male", you can add:

allow(@family).to receive(:location).with("male").and_return('his_work')

There are lots of different matching argument types that can be used with .with:

https://relishapp.com/rspec/rspec-mocks/v/3-2/docs/setting-constraints/matching-arguments

Upvotes: 0

thisismydesign
thisismydesign

Reputation: 25162

The accepted solution should only be used if you have a specific number of calls and need a specific sequence of data. But what if you don't know the number of calls that will be made, or don't care about the order of data only that it's something different each time? As OP said:

simply returning different values in a sequence would be OK

The issue with and_return is that the return value is memoized. Meaning even if you'd return something dynamic you'll always get the same.

E.g.

allow(mock).to receive(:method).and_return(SecureRandom.hex)
mock.method # => 7c01419e102238c6c1bd6cc5a1e25e1b
mock.method # => 7c01419e102238c6c1bd6cc5a1e25e1b

Or a practical example would be using factories and getting the same IDs:

allow(Person).to receive(:create).and_return(build_stubbed(:person))
Person.create # => Person(id: 1)
Person.create # => Person(id: 1)

In these cases you can stub the method body to have the code executed every time:

allow(Member).to receive(:location) do
  { residence: Faker::Address.city }
end
Member.location # => { residence: 'New York' }
Member.location # => { residence: 'Budapest' }

Note that you have no access to the Member object via self in this context but can use variables from the testing context.

E.g.

member = build(:member)
allow(member).to receive(:location) do
  { residence: Faker::Address.city, work: member.male? 'his_work' : 'her_work' }
end

Upvotes: 14

matteo
matteo

Reputation: 2256

I've tried the solution outline here above but it does not work for my. I solved the problem by stubbing with a substitute implementation.

Something like:

@family.stub(:location) { rand.to_s }

Upvotes: 0

ndnenkov
ndnenkov

Reputation: 36110

If for some reason you want to use the old syntax, you can still:

@family.stub(:location).and_return('foo', 'bar')

Upvotes: 1

idlefingers
idlefingers

Reputation: 32067

You can stub a method to return different values each time it's called;

allow(@family).to receive(:location).and_return('first', 'second', 'other')

So the first time you call @family.location it will return 'first', the second time it will return 'second', and all subsequent times you call it, it will return 'other'.

Upvotes: 225

nothing-special-here
nothing-special-here

Reputation: 12618

RSpec 3 syntax:

allow(@family).to receive(:location).and_return("abcdefg", "bcdefgh")

Upvotes: 21

Related Questions