petitviolet blog

    allow_any_instance_of to count up number of method calls behind the scene

    2021-08-25

    Ruby

    Rspec is one of the most popular test framework in Ruby, and of course it offers lots of functionalities for being able to write various type of test cases, like mock, stub, matcher, etc. allow_any_instance_of is a powerful method, which is able to stub any instance of a class even though using it is not encouraged.

    https://github.com/rspec/rspec-mocks#settings-mocks-or-stubs-on-any-instance-of-a-class

    Problem

    The problem I want to solve is counting up the number of calls of a method. I thought it can be done easily with .exactly(N).times matcher, but it can't.

    When you have Hoge class with hoge method.

    class Hoge
      def hoge
      end
    end
    

    Then, write a small spec.

    it 'fails with exactly matcher' do
      expect_any_instance_of(Hoge).to receive(:hoge).exactly(3).times
      
      Hoge.new.hoge
      Hoge.new.hoge
      Hoge.new.hoge
    end
    

    This spec fails due to the following error.

    Failure/Error: Hoge.new.hoge
      The message 'hoge' was received by #<Hoge:1640 > but has already been received by #<Hoge:0x00007fce52827968>
    

    The reason of this failure is that expect_any_instance_of is to set an expectation on an instance. It means it can't set expectations across more than one instances.

    How to solve

    To avoid the error, give a proc to count up method calls locally.

    it 'succeeds with giving proc to count up' do
      n = 0
      allow_any_instance_of(Hoge).to receive(:hoge) { n += 1 }
    
      Hoge.new.hoge
      Hoge.new.hoge
      Hoge.new.hoge
      expect(n).to eq(3)
    end
    

    This should succeed.

    In addition, you might think why I didn't use let to declare a variable within a spec, however, let doesn't work expectedly in this case because let actually declare not a variable but a function. Thus, the following snippet returns undefined method '+' for nil:NilClass error since it attempts to use n as a variable which is defined but not a variable

    let(:n) { 0 }
    
    it 'succeeds with giving proc to count up' do
      allow_any_instance_of(Hoge).to receive(:hoge) { n += 1 }
    
      Hoge.new.hoge
      Hoge.new.hoge
      Hoge.new.hoge
      expect(count).to eq(3)
    end
    

    You can see the entire source code at Gist: https://gist.github.com/petitviolet/9953af0aa561ea49c5e2aa13dd52f2ef