如何用RSpec检测回调中其他User对象的destroy_copy_cat方法调用?
这个问题的核心在于对象引用不匹配——你测试代码里的cc_user和实际执行destroy_copy_cat的实例不是同一个对象,所以RSpec的实例级调用断言会失败。让我一步步拆解原因和解决方案:
为什么断言失败?
当你执行cc_user = User.where(type: :admin, create_copy: false).first时,RSpec会从数据库加载一个全新的User实例对象。而在user.destroy!触发的destroy_copy_cat方法里,User.where(type: self.type).destroy_all会再次查询数据库,加载另一个独立的User实例(虽然数据库记录是同一个,但内存里是两个不同的对象引用)。
RSpec的expect(cc_user).to receive(:destroy_copy_cat)只会监听你声明的那个特定实例的方法调用,对数据库新加载的实例无效——哪怕它们对应同一条数据库记录。
解决方案
方法1:强制让数据库查询返回你的测试实例
最精准的方式是存根User.where方法,让它返回你预先创建的cc_user实例,这样代码中后续的查询会直接使用这个实例,断言就能命中:
# Test case 2 user = User.create!(type: :admin, create_copy: true) cc_user = User.create!(type: :admin, create_copy: false) # 先创建副本,避免依赖save回调 # 存根查询,让它返回我们的cc_user实例 allow(User).to receive(:where).with(type: :admin, create_copy: false).and_return([cc_user]) expect(user).to receive(:destroy_copy_cat) expect(cc_user).to receive(:destroy_copy_cat) # 现在这个断言会生效 user.destroy!
方法2:使用expect_any_instance_of(快速但不够精准)
如果你不需要严格指定哪个实例调用方法,只是想验证该方法被调用过,可以用expect_any_instance_of:
# Test case 2 user = User.create!(type: :admin, create_copy: true) cc_user = User.where(type: :admin, create_copy: false).first expect(user).to receive(:destroy_copy_cat) expect_any_instance_of(User).to receive(:destroy_copy_cat) # 匹配任意User实例的调用 user.destroy!
⚠️ 注意:官方不推荐过度使用expect_any_instance_of,因为它会模糊测试的精准性——如果有多个User实例调用该方法,这个断言无法区分是哪个实例触发的。
方法3:间接验证(通过数据库状态)
如果你的核心需求是验证副本被销毁,而不是严格验证方法调用,可以直接检查数据库状态:
# Test case 2 user = User.create!(type: :admin, create_copy: true) cc_user = User.where(type: :admin, create_copy: false).first expect { user.destroy! }.to change { User.where(id: cc_user.id).exists? }.from(true).to(false)
这种方式更贴近业务逻辑的最终效果,有时候比验证方法调用更有意义。
内容的提问来源于stack exchange,提问作者Ankur Gupta

