rspec 测试 has_many :through 和 after_save [英] rspec testing has_many :through and after_save
问题描述
我有一个(我认为)与连接表相对简单的 has_many :through
关系:
I have an (I think) relatively straightforward has_many :through
relationship with a join table:
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 测试(我知道这些不一定是好的测试,这些只是为了说明正在发生的事情):
And these rspec tests (I know these are not necessarily good tests, these are just to illustrate what's happening):
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_save
添加到引用其 followers
的 Thing
模型.也就是说,如果我这样做
This works fine UNTIL I add an after_save
to the Thing
model that references its followers
. That is, if I do
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
在模型中是空的).如果我在 followers.each
行之前在回调中添加一个 puts "HI"
,HI"会显示在 stdout 上,所以我知道正在调用回调.如果我注释掉 followers.each
行,则测试再次通过.
Then the second test fails - i.e., the relationship is still added to the join table, but @thing.followers
returns an empty array. Furthermore, that part of the callback never gets called (as if followers
is empty within the model). If I add a puts "HI"
in the callback before the followers.each
line, the "HI" shows up on stdout, so I know the callback is being called. If I comment out the followers.each
line, then the tests pass again.
如果我通过控制台完成这一切,它工作正常.即,我可以做到
If I do this all through the console, it works fine. I.e., I can do
>> 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 中失败?我做错了什么吗?
Why is this failing in rspec? Am I doing something horribly wrong?
更新
如果我手动将 Thing#followers
定义为
Everything works if I manually define Thing#followers
as
def followers
user_following_thing_relationships.all.map{ |r| r.user }
end
这让我相信,也许我用 :source
错误地定义了我的 has_many :through
?
This leads me to believe that perhaps I am defining my has_many :through
with :source
incorrectly?
更新
我创建了一个最小的示例项目并将其放在 github 上:https://github.com/dantswain/RspecHasMany
I've created a minimal example project and put it on github: https://github.com/dantswain/RspecHasMany
另一个更新
非常感谢@PeterNixey 和@kikuchiyo 在下面提供的建议.结果证明最终答案是两个答案的结合,我希望我可以在它们之间分配功劳.我已经用我认为最干净的解决方案更新了 github 项目并推送了更改:https://github.com/dantswain/RspecHasMany
Thanks a ton to @PeterNixey and @kikuchiyo for their suggestions below. The final answer turned out to be a combination of both answers and I wish I could split credit between them. I've updated the github project with what I think is the cleanest solution and pushed the changes: https://github.com/dantswain/RspecHasMany
如果有人能给我一个真正可靠的解释这里发生的事情,我仍然会喜欢它.对我来说最麻烦的一点是,为什么在最初的问题陈述中,如果我注释掉对 followers
的引用,一切(回调本身的操作除外)都会起作用.
I would still love it if someone could give me a really solid explanation of what is going on here. The most troubling bit for me is why, in the initial problem statement, everything (except the operation of the callback itself) would work if I commented out the reference to followers
.
推荐答案
过去我遇到过类似的问题,这些问题已通过重新加载关联(而不是父对象)解决.
I've had similar problems in the past that have been resolved by reloading the association (rather than the parent object).
如果您在 RSpec 中重新加载 thing.followers
是否有效?
Does it work if you reload thing.followers
in the RSpec?
it "should have followers" do
@thing.followers.reload
@thing.followers.should == [@user]
end
编辑
如果(正如您提到的)您在回调没有被触发时遇到问题,那么您可以在对象本身中重新加载:
If (as you mention) you're having problems with the callbacks not getting fired then you could do this reloading in the object itself:
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 没有重新加载关联的问题,但我自己也遇到了相同类型的问题
I don't know why RSpec has issues with not reloading associations but I've hit the same types of problems myself
编辑 2
虽然@dantswain 确认 followers.reload
有助于缓解一些问题,但它仍然没有解决所有问题.
Although @dantswain confirmed that the followers.reload
helped alleviate some of the problems it still didn't fix all of them.
要做到这一点,解决方案需要@kikuchiyo 的修复,它需要在Thing
中执行回调后调用save
:
To do that, the solution needed a fix from @kikuchiyo which required calling save
after doing the callbacks in Thing
:
describe Thing do
before :each do
...
@user.things << @thing
@thing.run_callbacks(:save)
end
...
end
最终建议
我相信这是因为在 has_many_through
操作上使用了 <<
.我不认为 <<
实际上应该触发您的 after_save
事件:
I believe this is happening because of the use of <<
on a has_many_through
operation. I don't see that the <<
should in fact trigger your after_save
event at all:
您当前的代码是这样的:
Your current code is this:
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
没有被调用.我认为这是正确的行为.
and the problem is that the do_stuff
is not getting called. I think this is the correct behaviour though.
让我们来看看 RSpec:
Let's go through the 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
对象——它只是在连接表中创建一个条目.
The problem is that the third step does not actually require the thing
object to be resaved - its simply creating an entry in the join table.
如果你想确保@user 确实调用了 save 你可能会得到你想要的效果:
If you'd like to make sure that the @user does call save you could probably get the effect you want like this:
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
在创建新关系时更新用户.
You may also find that the after_save
callback is in fact on the wrong object and that you'd prefer to have it on the relationship object instead. Finally, if the callback really does belong on the user and you do need it to fire after creating the relationship you could use touch
to update the user when a new relationship is created.
这篇关于rspec 测试 has_many :through 和 after_save的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!