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)