如何测试是否对从 Rails 和 RSpec 中的数据库中提取的特定对象调用了方法?

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

如果我有一个包含方法 dangerous_actionUser 模型,并且在某处我有代码在数据库中的特定用户子集上调用该方法,如下所示:

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

如何使用 RSpec 测试它是否在正确的用户上调用该方法,而不实际调用该方法?

我试过这个:

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

但是,当然,错误是 User 对象没有响应 has_recieved? 因为它不是 double 因为它是从数据库中提取的对象。

我想我可以通过猴子修补 dangerous_action 方法并使其写入全局变量来完成这项工作,然后在测试结束时检查全局变量的值,但我认为那将是一种非常丑陋的方式。有没有更好的方法?

我意识到我真的在尝试测试 perform_dangerous_action 方法的两个方面。第一个是数据库提取的范围,第二个是它对出现的用户对象调用正确的方法。

为了测试数据库获取的范围,我真的应该在 User class:

中创建一个范围
scope :not_fred, -> { where.not(name: "Fred") }

可以通过单独的测试轻松测试。

那么perform_dangerous_action方法就变成了

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

检查它为 not_fred 用户调用正确方法的测试是

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

我认为,在许多情况下,您不想将 wherewhere.not 分成一个范围,在这种情况下,您可以存根 ActiveRecord::Relation 本身,例如如:

# 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)

在你的情况下,where.not 实际上是调用 ActiveRecord::QueryMethods::WhereChain#not 方法,所以我可以做

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