rspec测试has_many:through和after_save

我有一个(我认为)相对简单的has_many :through与连接表的关系:

 class User  :user_following_thing_relationships end class Thing  :user_following_thing_relationships, :source => :user end class UserFollowingThingRelationship < ActiveRecord::Base belongs_to :thing belongs_to :user end 

这些rspec测试(我知道这些不一定是好的测试,这些只是为了说明发生了什么):

 describe Thing do before(:each) do @user = User.create!(:name => "Fred") @thing = Thing.create!(:name => "Foo") @user.things << @thing end it "should have created a relationship" do UserFollowingThingRelationship.first.user.should == @user UserFollowingThingRelationship.first.thing.should == @thing end it "should have followers" do @thing.followers.should == [@user] end end 

这很好用UNTIL我将一个after_save添加到引用其followersThing模型中。 也就是说,如果我这样做

 class Thing  :user_following_thing_relationships, :source => :user def do_stuff followers.each { |f| puts "I'm followed by #{f.name}" } end end 

然后第二个测试失败 – 即,关系仍然添加到连接表,但@thing.followers返回一个空数组。 此外,回调的那部分永远不会被调用(好像followers在模型中是空的)。 如果我在followers.each行之前的回调中添加一个puts "HI" ,那么“HI”会出现在stdout上,所以我知道正在调用回调。 如果我注释掉了followers.each行,那么测试再次通过。

如果我通过控制台完成所有操作,它可以正常工作。 即,我能做到

 >> t = Thing.create!(:name => "Foo") >> t.followers # [] >> u = User.create!(:name => "Bar") >> u.things <> t.followers # [u] >> t.save # just to be super duper sure that the callback is triggered >> t.followers # still [u] 

为什么这在rspec中失败了? 我做错了什么吗?

更新

如果我手动将Thing#followers定义为,那么一切正常

 def followers user_following_thing_relationships.all.map{ |r| r.user } end 

这让我相信也许我正在定义我的has_many :through :source错误?

更新

我创建了一个最小的示例项目并将其放在github上: https : //github.com/dantswain/RspecHasMany

另一个更新

非常感谢@PeterNixey和@kikuchiyo提供以下建议。 最后的答案结果是两个答案的组合,我希望我可以在他们之间分配信用。 我用我认为最干净的解决方案更新了github项目并推动了更改: https : //github.com/dantswain/RspecHasMany

如果有人能给我一个关于这里发生了什么的非常可靠的解释,我仍然会喜欢它。 对我来说最令人不安的是,为什么在最初的问题陈述中,如果我注释了对followers的引用,那么一切(除了回调本身的操作)都会有效。

我过去曾遇到类似的问题,通过重新加载关联(而不是父对象)解决了这个问题。

如果您在RSpec中重新加载thing.followers ,它是否有效?

 it "should have followers" do @thing.followers.reload @thing.followers.should == [@user] end 

编辑

如果(正如你所提到的)你遇到了没有被解雇的回调问题,那么你可以在对象本身中重新加载:

 class Thing < ActiveRecord::Base after_save { followers.reload} after_save :do_stuff ... end 

要么

 class Thing < ActiveRecord::Base ... def do_stuff followers.reload ... end end 

我不知道为什么RSpec没有重新加载关联的问题,但我自己也遇到了同样类型的问题

编辑2

尽管@dantswain确认了followers.reload有助于缓解一些问题,但它仍然没有解决所有这些问题。

要做到这一点,解决方案需要@kikuchiyo的修复,这需要在Thing执行回调后调用save

 describe Thing do before :each do ... @user.things << @thing @thing.run_callbacks(:save) end ... end 

最后的建议

我相信这是因为在has_many_through操作中使用<< 。 我没有看到<<应该实际上应该触发你的after_save事件:

您当前的代码是这样的:

 describe Thing do before(:each) do @user = User.create!(:name => "Fred") @thing = Thing.create!(:name => "Foo") @user.things << @thing end end class Thing < ActiveRecord::Base after_save :do_stuff ... def do_stuff followers.each { |f| puts "I'm followed by #{f.name}" } end end 

问题是do_stuff没有被调用。 我认为这是正确的行为。

我们来看看RSpec:

 describe Thing do before(:each) do @user = User.create!(:name => "Fred") # user is created and saved @thing = Thing.create!(:name => "Foo") # thing is created and saved @user.things << @thing # user_thing_relationship is created and saved # no call is made to @user.save since nothing is updated on the user end end 

问题是第三步实际上并不需要重新保存thing对象 - 它只是在连接表中创建一个条目。

如果你想确保@user确实调用了save,你可能会得到你想要的效果:

 describe Thing do before(:each) do @thing = Thing.create!(:name => "Foo") # thing is created and saved @user = User.create!(:name => "Fred") # user is created BUT NOT SAVED @user.things << @thing # user_thing_relationship is created and saved # @user.save is also called as part of the addition end end 

您可能还会发现after_save回调实际上是在错误的对象上,而您更喜欢将它放在关系对象上。 最后,如果回调确实属于用户,并且您确实需要在创建关系后触发它,则可以在创建新关系时使用touch来更新用户。

更新的答案**这会传递rspec,没有存根,运行save的回调(包括after_save回调),并在尝试访问其元素之前检查@ thing.followers是否为空。 (;

 describe Thing do before :each do @user = User.create(:name => "Fred"); @thing = Thing.new(:name => 'Foo') @user.things << @thing @thing.run_callbacks(:save) end it "should have created a relationship" do @thing.followers.should == [@user] puts @thing.followers.inspect end end class Thing < ActiveRecord::Base after_save :some_function has_many :user_following_thing_relationships has_many :followers, :through => :user_following_thing_relationships, :source => :user def some_function the_followers = followers unless the_followers.empty? puts "accessing followers here: the_followers = #{the_followers.inspect}..." end end end 

原始答案**

我能够使用after_save回调来处理事情,只要我没有在do_stuff的body / block中引用followers 。 您是否必须使用after_save中的实际方法引用followers

更新了用于存根回调的代码。 现在模型可以保留你需要它,我们显示@ thing.followers确实按照我们的预期设置,我们可以通过after_save在不同的规范中调查do_stuff / some_function的function。

我在这里推了一份代码: https : //github.com/kikuchiyo/RspecHasMany

并且规范传递的东西*代码如下:

 # thing_spec.rb require 'spec_helper' describe Thing do before :each do Thing.any_instance.stub(:some_function) { puts 'stubbed out...' } Thing.any_instance.should_receive(:some_function).once @thing = Thing.create(:name => "Foo"); @user = User.create(:name => "Fred"); @user.things << @thing end it "should have created a relationship" do @thing.followers.should == [@user] puts @thing.followers.inspect end end # thing.rb class Thing < ActiveRecord::Base after_save :some_function has_many :user_following_thing_relationships has_many :followers, :through => :user_following_thing_relationships, :source => :user def some_function # well, lets me do this, but I cannot use @x without breaking the spec... @x = followers puts 'testing puts hear shows up in standard output' x ||= 1 puts "testing variable setting and getting here: #{x} == 1\n\t also shows up in standard output" begin # If no stubbing, this causes rspec to fail... puts "accessing followers here: @x = #{@x.inspect}..." rescue puts "and this is but this is never seen." end end end 

我的猜测是你需要通过执行@thing.reload来重新加载你的Thing实例(我确信有一种方法可以避免这种情况,但是这可能会让你的测试首先通过,然后你可以找出你出错的地方)。

几个问题:

我没有看到你在你的规范中调用@thing.save 。 你是这么做的,就像你的控制台示例一样?

为什么你在控制台测试中调用t.save而不是u.save ,考虑到你正在推动u ? 保存u应该触发一个保存到t ,得到你想要的最终结果,并且我认为考虑到你真的在努力,而不是t ,它会“更有意义”。