How to spy just one call of ActiveSupport::Notifications #instrument, not all of them

1k Views Asked by At

I'm making a Rspec test that checks if ActiveSupport::Notification.instrument was called with some parameters.

The thing is that in order to make this test a need FactoryBot to build some objects, but when I try to spy on ActiveSupport::Notification.instrument I always get an error:

ActiveSupport::Notifications received :instrument with unexpected arguments
         expected: (:asd)
              got: ("factory_bot.run_factory", {:factory=>#<FactoryBot::Factory:0x005569b6d30, @al... nil, dispatch: nil, distribution_state: 2, main_category_id: nil>}, :strategy=>:build, :traits=>[]})

It seems that FactoryBot calls activesupport so when I mock it for my test purpose I end up mocking it too far...

code example:

class:

class SomeObject
    def initialize(something)
        #some code
    end

    def my_method
        ActiveSupport::Notifications.instrument :asd
    end
end

spec:

describe "#my_method" do
    let(:some_object) { build :some_object }
    before do
      allow(ActiveSupport::Notifications).to receive(:instrument).with :asd
    end

    it "calls notifier" do
      described_class.new(some_object).my_method

      expect(ActiveSupport::Notifications).to have_received(:instrument).with :asd
    end
  end

How can I just mock my call and not FactoryBot's .

I only manage that with one more allow before the one that mocks :asd:

 allow(ActiveSupport::Notifications).to receive(:instrument).and_call_original

Is there another(better) way?

1

There are 1 best solutions below

0
John Gallagher On BEST ANSWER

I tend to avoid mocking in general.

I had a similar problem and here's how I achieved it:

  describe "#my_method" do
    let(:some_object) { build :some_object }

    before { record_events }

    it "calls notifier" do
      described_class.new(some_object).my_method

      # Make sure your event was triggered
      expect(events.map(&:name)).to include('asd')

      # Check number of events
      expect(events).to be_one

      # Check contents of event payload                  
      expect(events.first.payload).to eq({ 'extra' => 'context' })

      # Even check the duration of an event
      expect(events.first.duration).to be < 3
    end

    private

    attr_reader :events

    def record_events
      @events = []
      ActiveSupport::Notifications.subscribe(:asd) do |*args| #
        @events << ActiveSupport::Notifications::Event.new(*args)
      end
    end
  end

Advantages over mocking

  • No more weird side effects
  • Using ActiveSupport::Notifications as intended
  • ActiveSupport::Notifications::Event wrapper gives you nice extras like #duration
  • Easily check for other events being triggered
  • The ability to only look at events that match a name - use ActiveSupport::Notifications.subscribe(/asd/) to do partial matches on event names
  • Better readability - checking events array is more readable

Disadvantages over mocking

  • Way more code
  • Mutates the @events array
  • Possible dependencies between tests if you don't clear @events on teardown