Home > Blockchain >  How do I test if a method is called on particular objects pulled from the DB in Rails with RSpec?
How do I test if a method is called on particular objects pulled from the DB in Rails with RSpec?

Time:10-04

If I have a User model that includes a method dangerous_action and somewhere I have code that calls the method on a specific subset of users in the database like this:

class UserDanger
  def perform_dangerous_action
    User.where.not(name: "Fred").each(&:dangerous_action)
  end
end

how do I test with RSpec whether it's calling that method on the correct users, without actually calling the method?

I've tried this:

it "does the dangerous thing, but not on Fred" do
  allow_any_instance_of(User).to receive(:dangerous_action).and_return(nil)
  u1 = FactoryBot.create(:user, name: "Jill")
  u2 = FactoryBot.create(:user, name: "Fred")
  UserDanger.perform_dangerous_action
  expect(u1).to have_recieved(:dangerous_action)
  expect(u2).not_to have_recieved(:dangerous_action)
end

but, of course, the error is that the User object doesn't respond to has_recieved? because it's not a double because it's an object pulled from the database.

I think I could make this work by monkey-patching the dangerous_action method and making it write to a global variable, then check the value of the global variable at the end of the test, but I think that would be a really ugly way to do it. Is there any better way?

CodePudding user response:

I realised that I'm really trying to test two aspects of the perform_dangerous_action method. The first is the scoping of the database fetch, and the second is that it calls the correct method on the User objects that come up.

For testing the scoping of the DB fetch, I should really just make a scope in the User class:

scope :not_fred, -> { where.not(name: "Fred") }

which can be easily tested with a separate test.

Then the perform_dangerous_action method becomes

def perform_dangerous_action
  User.not_fred.each(&:dangerous_action)
end

and the test to check it calls the right method for not_fred users is

it "does the dangerous thing" do
  user_double = instance_double(User)
  expect(user_double).to receive(:dangerous_action)
  allow(User).to receive(:not_fred).and_return([user_double])
  UserDanger.perform_dangerous_action
end

CodePudding user response:

i think, in many cases, you don't want to separate a where or where.not into a scope, in that cases, you could stub ActiveRecord::Relation itself, such as:

# default call_original for all normal `where`
allow_any_instance_of(ActiveRecord::Relation)
.to receive(:where).and_call_original

# stub special `where`
allow_any_instance_of(ActiveRecord::Relation)
.to receive(:where).with(name: "...")
.and_return(user_double)

in your case, where.not is actually call ActiveRecord::QueryMethods::WhereChain#not method so i could do

allow_any_instance_of(ActiveRecord::QueryMethods::WhereChain)
.to receive(:not).with(name: "Fred")
.and_return(user_double)
  • Related