rspec测试has_many:through和after_save

dan*_*ain 5 ruby rspec ruby-on-rails after-save has-many-through

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

class User < ActiveRecord::Base
  has_many :user_following_thing_relationships
  has_many :things, :through => :user_following_thing_relationships
end

class Thing < ActiveRecord::Base
  has_many :user_following_thing_relationships
  has_many :followers, :through => :user_following_thing_relationships, :source => :user
end

class UserFollowingThingRelationship < ActiveRecord::Base
  belongs_to :thing
  belongs_to :user
end
Run Code Online (Sandbox Code Playgroud)

这些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
Run Code Online (Sandbox Code Playgroud)

这工作正常UNTIL我添加一个引用它after_saveThing模型followers.也就是说,如果我这样做

class Thing < ActiveRecord::Base
  after_save :do_stuff
  has_many :user_following_thing_relationships
  has_many :followers, :through => :user_following_thing_relationships, :source => :user

  def do_stuff
    followers.each { |f| puts "I'm followed by #{f.name}" }
  end
end
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

更新

如果我手动定义Thing#followers为,一切都有效

def followers
  user_following_thing_relationships.all.map{ |r| r.user }
end
Run Code Online (Sandbox Code Playgroud)

这使我相信,也许我定义我has_many :through:source错误?

更新

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

另一个更新

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

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

Pet*_*xey 8

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

如果你thing.followers在RSpec上重装,它会起作用吗?

it "should have followers" do
  @thing.followers.reload
  @thing.followers.should == [@user]
end 
Run Code Online (Sandbox Code Playgroud)

编辑

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

class Thing < ActiveRecord::Base
  after_save { followers.reload}
  after_save :do_stuff
  ...
end
Run Code Online (Sandbox Code Playgroud)

要么

class Thing < ActiveRecord::Base
  ...
  def do_stuff
    followers.reload
    ...
  end
end
Run Code Online (Sandbox Code Playgroud)

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

编辑2

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

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

describe Thing do
  before :each do
    ...
    @user.things << @thing
    @thing.run_callbacks(:save)
  end 
  ...
end
Run Code Online (Sandbox Code Playgroud)

最后的建议

我相信这是因为<<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
Run Code Online (Sandbox Code Playgroud)

而问题是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
Run Code Online (Sandbox Code Playgroud)

问题是第三步实际上并不需要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
Run Code Online (Sandbox Code Playgroud)

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

  • 我认为彼得更好.你应该给他赏金.我可以获得投票吗?:P.感谢提出问题和彼得的答案,我也从中学到了答案. (2认同)