Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

rspec testing has_many :through and after_save

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.

like image 225
dantswain Avatar asked Jan 18 '12 16:01

dantswain


People also ask

What is difference between Has_many through and Has_and_belongs_to_many?

The Difference Between HMT and HABTM Aside from the actual code you write, the main difference between the two approaches is that in a has_many :through relationship, the JOIN table has its own model, while a has_and_belongs_to_many relationship has no JOIN model, just a database table.

How do I run a specific test in RSpec?

To run a single Rspec test file, you can do: rspec spec/models/your_spec. rb to run the tests in the your_spec. rb file.

Is RSpec TDD or BDD?

RSpec is a Behavior-Driven Development tool for Ruby programmers. BDD is an approach to software development that combines Test-Driven Development, Domain Driven Design and Acceptance Test-Driven Planning. RSpec helps you do the TDD part of that equation, focusing on the documentation and design aspects of TDD.


1 Answers

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.

like image 132
Peter Nixey Avatar answered Sep 22 '22 07:09

Peter Nixey