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
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
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
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]
Why is this failing in rspec? Am I doing something horribly wrong?
Update
Everything works if I manually define Thing#followers
as
def followers
user_following_thing_relationships.all.map{ |r| r.user }
end
This leads me to believe that perhaps I am defining my has_many :through
with :source
incorrectly?
Update
I've created a minimal example project and put it on github: https://github.com/dantswain/RspecHasMany
Another Update
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
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).
Does it work if you reload thing.followers
in the RSpec?
it "should have followers" do
@thing.followers.reload
@thing.followers.should == [@user]
end
EDIT
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
or
class Thing < ActiveRecord::Base
...
def do_stuff
followers.reload
...
end
end
I don't know why RSpec has issues with not reloading associations but I've hit the same types of problems myself
Edit 2
Although @dantswain confirmed that the followers.reload
helped alleviate some of the problems it still didn't fix all of them.
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
Final suggestion
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
and the problem is that the do_stuff
is not getting called. I think this is the correct behaviour though.
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
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.
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
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.
UPDATED ANSWER ** This passes rspec, without stubbing, running callbacks for save (after_save callback included ), and checks that @thing.followers is not empty before trying to access its elements. (;
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
ORIGINAL ANSWER **
I was able to get things to work with the after_save callback, so long as I did not reference followers
within the body / block of do_stuff
. Do you have to reference followers
in the real method you are calling from after_save
?
Updated code to stub out callback. Now model can remain as you need it, we show @thing.followers is indeed set as we expected, and we can investigate the functionality of do_stuff / some_function via after_save in a different spec.
I pushed a copy of the code here: https://github.com/kikuchiyo/RspecHasMany
And spec passing thing* code is below:
# 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
My guess is that you need to reload your Thing
instance by doing @thing.reload
(I'm sure there's a way to avoid this, but that might get your test passing at first and then you can figure out where you've gone wrong).
Few questions:
I don't see you calling @thing.save
in your spec. Are you doing that, just like in your console example?
Why are you calling t.save
and not u.save
in your console test, considering you're pushing t
onto u
? Saving u
should trigger a save to t
, getting the end result you want, and I think it would "make more sense" considering you are really working on u
, not t
.
来源:https://stackoverflow.com/questions/8914121/rspec-testing-has-many-through-and-after-save