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
添加到引用其followers
的Thing
模型中。 也就是说,如果我这样做
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
,它会“更有意义”。