RSpec の expect_any_instance_of でハマった

class Test
  def say_hoge
    hoge1 = Hoge.new
    hoge2 = Hoge.new
    hoge1.say
    hoge2.say
  end
end

class Hoge
  def say
    puts 'hoge'
  end
end

こんな感じのクラスがあったとして、 Test#say_hogeHoge インスタンスHoge#say を1回ずつ実行していることをテストしたいとなったときに、以下のようなテストを書きました。

describe 'Test' do
  describe '#say_hoge' do
    it 'Hoge インスタンス は Hoge#say を1回ずつ実行する' do
      expect_any_instance_of(Hoge).to receive(:say).once
      test = Test.new
      test.say_hoge
    end
  end
end

これを実行してみたところ、落ちてしまいました。


ああ、呼ばれている回数は全インスタンスの合計になるのか、なるほどね!とか思って、以下のように once を twice に変えてみました。

describe 'Test' do
  describe '#say_hoge' do
    it 'Hoge インスタンス は Hoge#say を1回ずつ実行する' do
      expect_any_instance_of(Hoge).to receive(:say).twice # 合計2回実行されるので twice に変えてみた
      test = Test.new
      test.say_hoge
    end
  end
end

これを実行してみたところ、またまた落ちてしまいました。


なぜだと思ってエラーを見てみると、「Hoge#say を hoge2 が実行したんだけど、 hoge1 でもう実行されてるよ」 的なことが書いてありました。

  1) Test #say_hoge Hoge instance call Hoge#say once
     Failure/Error: hoge2.say
       The message 'say' was received by #<Hoge:70248456891000 > but has already been received by #<Hoge:0x007fc7fca35518>

ためしに、以下のように hoge2.say を消して、 twice を once に戻してみました。

class Test
  def say_hoge
    hoge1 = Hoge.new
    hoge2 = Hoge.new
    hoge1.say
    # hoge2.say
  end
end
describe 'Test' do
  describe '#say_hoge' do
    it 'Hoge インスタンス は Hoge#say を1回ずつ実行する' do
      expect_any_instance_of(Hoge).to receive(:say).once
      test = Test.new
      test.say_hoge
    end
  end
end

これを実行すると、ちゃんと通りました。

わかったこと

おそらく、 expect_any_instance_of はどのインスタンスも対象にとるが、対象のインスタンスは1つに限るということなのだと思います。

どうするか

そのため、stub を使って Hoge.new でつくられるインスタンスを同じにして、そのインスタンスHoge#say が合計2回呼ばれるということをテストすることにしました。

class Test
  def say_hoge
    hoge1 = Hoge.new
    hoge2 = Hoge.new
    hoge1.say
    hoge2.say
  end
end
describe 'Test' do
  describe '#say_hoge' do
    it 'Hoge インスタンス は Hoge#say を1回ずつ実行する (合計2回) ' do
      hoge = Hoge.new
      allow(Hoge).to receive(:new).and_return(hoge)
      expect(hoge).to receive(:say).twice
      test = Test.new
      test.say_hoge
    end
  end
end

これを実行したところ無事通りました。

Finished in 0.01246 seconds (files took 0.42959 seconds to load)
1 example, 0 failures