Search code examples
ruby-on-railsrspectddrspec-rails

RSpec: Method test will not pass unless method called therein is also tested?


I'm new to testing with RSpec and FactoryBot so any help would be appreciated. I've encountered an odd case with the following code/tests.

Here are my models:

class Foo < ActiveRecord::Base
    has_many :bars, dependent: :destroy

    def update_baz_count(baz_count)
        most_recent_bar.update_current_baz_count(baz_count)
    end

    def most_recent_bar
       bars.last
    end
end

class Bar < ActiveRecord::Base
  belongs_to :foo

  def update_current_baz_count(new_baz_count)
    self.baz_count = new_baz_count
    self.save
  end
end

And here are my tests:

describe Foo do
  # This test passes
  describe "#most_recent_bar" do
    let!(:foo) { create(:foo) }
    let!(:bar) { create(:bar, foo: foo) }

    it 'should return the most recent bar' do
      expect(foo.most_recent_bar).to eq(bar)
    end
  end

  describe '#update_baz_count' do
    let!(:foo) { create(:foo) }
    let!(:bar) { create(:bar, foo: foo) }

    it 'should call update_current_bar_count on the storage history' do
      ## Test will NOT pass unless following line is uncommented:
      # expect(foo).to receive(:most_recent_bar).and_return(bar) 
      expect(bar).to receive(:update_current_baz_count).with(1)
      foo.update_baz_count(1)
    end
  end
end

The issue is that in my #update_baz_count test passing is contingent on setting an expectation regarding the #most_recent_bar method. As noted above, my test for #most_recent_bar passes, and it feels redundant to be making assertions about the performance of that method outside of its dedicated test.

So, why is the success of my test contingent on the line expect(foo).to receive(:most_recent_bar).and_return(bar)?


Solution

  • The problem is that you set up the mocking behavior on the object available in specs:

    expect(bar).to receive(:update_current_baz_count).with(1)

    But! In your production code, the same row will be fetched from db:

    bars.last

    And AR will create a new object for you, which have no idea that you mocked it in your specs.

    You can check it out like this:

    expect(bar.object_id).to eq foo.most_recent_bar.object_id

    Which will fail.

    If you want to do it without mocking, do something like this:

    it 'should update update_current_bar_count on the storage history' do
    
    
      expect{ foo.update_baz_count(1) }
        .to change { bar.reload.field_that_baz_count_updates}.from(0).to(1)
    end
    

    So instead of checking that the method was called, check the effect the calling of the method had on the "world".