Jellicle
Jellicle

Reputation: 30286

Rspec: expect an association to receive a message

I'm in Rails 6 with Rspec 3.8.0.

I have a model A which belongs_to B. And I'm trying to write a unit test with A as subject:

expect(subject.b).to receive(:to_s)
subject.my_fn

Yet this spec always fails, saying that the instance of B did not receive the message, notwithstanding I have put binding.pry in the actual code to run and verified that a.b.to_s gets called:

class A
  def my_fn
    binding.pry
    b.to_s
  end
end

I have even tried:

expect(a).to receive(:b).and_return(b)
expect(b).to receive(:to_s)

And:

expect_any_instance_of(b.class).to receive(:to_s)

Yet all expectations for to_s fail. Why is this?

Upvotes: 2

Views: 2682

Answers (2)

Jay-Ar Polidario
Jay-Ar Polidario

Reputation: 6603

It's not shown in your code, but I have a feeling that you are calling the code before you set up your "receive" expectations. Simply put, the code execution should be like below:

it 'something' do
  expect(subject.b).to receive(:to_s)

  # write code here that would eventually call `a.b.to_s` (as you have said)
  # i.e.
  # `subject.some_method` (assuming `some_method` is your method that calls `a.b.to_s`
  # don't call `subject.some_method` before the `expect` block above.
end
  • Also, just in case you don't know yet, make sure that it's the same object instance that you pass in to expect: expect(THE_ARG) ... receive() and the object that you are testing to be called. You can verify that they are the same if they have the same object_id:
it 'something' do
  puts subject.b.object_id
  # => 123456789

  subject.some_method
end

# the class/method you're unit-testing:
class Foo
  def some_method
    # ...
    puts b.object_id
    # => 123456789
    # ^ should also be the same

Otherwise if it's not the same object (object_id does not match), you would have to either use expect_any_instance_of (which I only use at the last resort as it is potentially dangerous as it is expecting "any instance")... or you could stub the chain a.b.to_s objects in your spec file.

If it's hard to stub the whole chain but at the same time, avoid the pitfalls of using expect_any_instance_of, there's another variant that I use which I use to balance convenience and spec-accuracy:

it 'something' do
  expect_any_instance_of(subject.b.class).to receive(:to_s).once do |b|
    expect(b.id).to eq(subject.b.id)
    # the above just compares the `id` of the records (even if they are different objects in different memory-space)

    # to demonstrate, say I do puts here:
    puts b
    # => #<SomeRecord:0x00005600e7a6f3b8 id:1 ...>
    puts subject.b
    # => #<SomeRecord:0x00005600e4f04138 id:1 ...>
    puts b.id
    # => 1
    puts subject.b.id
    # => 1

    # notice that they are different objects (0x00005600e7a6f3b8 vs 0x00005600e4f04138)
    # but the attribute id is the same (1 == 1)
  end

  subject.some_method
end

Upvotes: 3

Maycon Seidel
Maycon Seidel

Reputation: 181

Seems that makes more sense you stub the b relation. It will looks like:

expect(a).to receive(:b).and_return(stub(:b, to_s: 'foo_bar')

Upvotes: 0

Related Questions