Factories, RSpec stubs, database queries, and you

  • May 10, 2018
  • The Gnar Company
  • 3 min read

by Andrew Palmer

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.