rspec testing has_many :through and after_save

前端 未结 3 1502
小鲜肉
小鲜肉 2021-02-06 11:20

I have an (I think) relatively straightforward has_many :through relationship with a join table:

class User < ActiveRecord::Base
  has_many :user         


        
相关标签:
3条回答
  • 2021-02-06 11:58

    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.

    0 讨论(0)
  • 2021-02-06 12:00

    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
    
    0 讨论(0)
  • 2021-02-06 12:08

    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.

    0 讨论(0)
提交回复
热议问题