RSpec 存根方法能否按顺序返回不同的值?

我有一个模型家族与方法 location,其中合并的 location输出的其他对象,成员。(成员与家庭有关,但这并不重要。)

例如,给定

  • Member _ 1 has location = = ‘ San Diego (travel,return 15 May)’
  • Member _ 2 has location = = ‘ San Diego’

Location 可能返回“ San Diego (member _ 1 travel,return 15 May)”具体细节并不重要。

为了简化 Family.location 的测试,我想存根 Member.location。但是,我需要它返回两个不同的(指定的)值,如上例所示。理想情况下,这些属性应该基于 member属性,但是简单地按顺序返回不同的值就可以了。在 RSpec 里有办法做到这一点吗?

可以在每个测试示例中重写 Member.location 方法,例如

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

但我怀疑这是不是个好习惯。在任何情况下,当 RSpec 运行一系列示例时,它都不会还原原始类,因此这种方法会覆盖后续示例。

那么,有没有一种方法可以让一个存根方法在每次调用中返回不同的、指定的值呢?

45799 次浏览

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'.

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 }

RSpec 3 syntax:

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

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

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

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