7

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

还有这些 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

这工作正常,直到我将一个添加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

然后第二个测试失败 - 即关系仍然添加到连接表,但@thing.followers返回一个空数组。此外,回调的那部分永远不会被调用(好像followers在模型中是空的)。如果我puts "HI"在该行之前的回调中添加一个followers.each,则“HI”会显示在标准输出上,所以我知道正在调用回调。如果我注释掉该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]

为什么这在 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.

4

3 回答 3

9

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

如果您thing.followers在 RSpec 中重新加载它会起作用吗?

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 的修复,该修复需要save在执行回调后调用Thing

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

最终建议

我相信这是因为使用了<<onhas_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新关系时使用它来更新用户。

于 2012-01-22T18:06:35.117 回答
2

更新的答案 ** 这会通过 rspec,没有存根,运行保存回调(包括 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

原始答案**

只要我没有followersdo_stuff. 您是否必须引用followers您调用的真实方法after_save

更新代码以存根回调。现在模型可以根据需要保留,我们显示 @thing.followers 确实按照我们的预期设置,我们可以通过 after_save 在不同的规范中研究 do_stuff / some_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
于 2012-01-22T00:10:19.547 回答
1

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

几个问题:

我没有看到你打电话给@thing.save你的规格。您是否正在这样做,就像在您的控制台示例中一样?

考虑到你正在推进,为什么你打电话t.save而不是在你的控制台测试中?保存应该触发保存到,得到你想要的最终结果,我认为考虑到你真的在工作,而不是.u.savetuutut

于 2012-01-18T17:35:01.613 回答