The Gnar Company
The Gnar Company

Factories, RSpec stubs, database queries, and you

I've run into this a couple of times in the past, and just ran into it again on my current project: I want to test that some method is called on an instance of some class I'm working with. That instance is being returned by a database query in some service, like so:

class SomeService
  ...
  def user
    @user ||= User.find(id)
  end

  def method
    user.do_the_thing
  end
  ...
end

In my test, I've created a user with FactoryBot, and I've stubbed the do_the_thing method so I can listen for when it's called:

let(:user) { create :user }

before do
  allow(user).to receive(:do_the_thing)
end

it "calls do_the_thing" do
  SomeService.new.method

  expect(user).to have_received(:do_the_thing)
end

Unfortunately, this fails, even though when you inspect the user being created by the factory and the one returned by the database query, they appear identical.

However, if you call user.object_id on both objects, you'll be able to see that the two are, despite having identical properties, not the same object.

In order to get this working, you also have to stub the database query to get it to return the factory-created user from the test:

let(:user) { create :user }

before do
  allow(user).to receive(:do_the_thing)
  allow(User).to receive(:find).and_return(user)
end

it "calls do_the_thing" do
  SomeService.new.method

  expect(user).to have_received(:do_the_thing)
end

This should pass.

How come?

The reason this happens is because stubbing the do_the_thing method on user adds a behavior to the object that represents a row in the User database, but isn't that record itself. Similarly, the database query in SomeService returns another representation in memory of the same row in the database, but also isn't that record itself.

As a result, even though both the FactoryBot user and the user returned by the database query are referring to the same thing, they're actually different objects in memory, one of which is stubbed to listen for a specific method call and the other of which is not.

This is basically the same as creating two variables, user_1 and user_2, that both point to the first record in the Users table. If we were to manipulate user_1 in some way, it would be surprising to learn that user_2 was modified as well:

User.create(name: "Sally")

user_1 = User.first
user_2 = User.first

user_1.name = "Jane"
user_2.name  # returns "Sally"

If user_2.name were to return "Jane", that would mean that we somehow managed to change one object in memory by changing an entirely different one, which wouldn't be good.

To put this back into terms of the original question, the following line:

let(:user) { create :user }

both creates a user record in the database and saves that record in a variable named user. We're then manipulating that variable by adding a behavior to it:

allow(user).to receive(:do_the_thing)

which is analogous to assigning "Jane" to the name property of that variable; the underlying database record itself isn't changing, just the variable that's referring to it.

As a result, if we want our database query to return an object that has a stubbed method associated with it, we have to explicity tell the database query to return the correct object:

allow(User).to receive(:find).and_return(user)

Now our database query is returning the same in-memory representation of the user as the one created by FactoryBot, which will include the stubbed method we added in the test and allow the test to pass.