Techioz Blog

RSpec: StandardError は動作するのに、他の例外クラスは動作しないのはなぜですか?

概要

一部のテストでは、特定の例外クラスを発生させるモックをセットアップしたいと考えています。この特定の例外をテストでインスタンス化するのは難しいため、double を使用したいと思います。

ここに例を示します。

class SomeError < StandardError
  def initialize(some, random, params)
    # ...
  end
end

class SomeClass
  def some_method
    mocked_method
    :ok
  rescue SomeError
    :ko
  end

  def mocked_method
    true
  end
end

describe SomeClass do
  subject(:some_class) { described_class.new }

  describe '#some_method' do
    subject(:some_method) { some_class.some_method }

    it { is_expected.to be :ok }

    context 'when #mocked_method fails' do
      before do
        allow(some_class).to receive(:mocked_method)
          .and_raise(instance_double(SomeError))
      end

      it { is_expected.to be :ko }
    end
  end
end

ただし、このテストの実行は次のメッセージが表示されて失敗します。

     Failure/Error:
       mocked_method

     TypeError:
       exception class/object expected

奇妙なのは、SomeError を StandardError に置き換えると、正常に動作することです。

class SomeClass
  def some_method
    mocked_method
    :ok
  rescue StandardError
    :ko
  end

  def mocked_method
    true
  end
end

describe SomeClass do
  subject(:some_class) { described_class.new }

  describe '#some_method' do
    subject(:some_method) { some_class.some_method }

    it { is_expected.to be :ok }

    context 'when #mocked_method fails' do
      before do
        allow(some_class).to receive(:mocked_method)
          .and_raise(instance_double(StandardError))
      end

      it { is_expected.to be :ko }
    end
  end
end

ここで何が起きてるの? StandardError をモックするときに、特殊なケースはありますか?あるいは、インスタンス化が難しい例外クラスをモックするより良い方法はありますか?

解決策

問題の説明

TypeError は Kernel#raise によって発生します。

RSpec::Mocks::MessageExpectation#and_raise は、後で呼び出される Proc (ここに表示) で Kernel#raise への呼び出しをラップします。

Kernel#raise は以下を受け入れます。

あなたの場合、instance_double(SomeError) は上記のどれでもないため、Kernel#raise は TypeError をスローします。同じことは次のように再現できます。

raise({a: 12})
in `raise': exception class/object expected (TypeError)

レッドニシン

StandardError が機能する理由は、あなたが考えているものではありません。

代わりに、SomeClass#some_method が StandardError をレスキューし、TypeError が StandardError (StandardError から継承) であるため、StandardError は単に機能しているように見えます。この場合、TypeError は依然として発生しており、プロセス内で救済されているだけです。

これを証明するには、and_raise(instance_double(StandardError)) を and_raise(instance_double(SomeError)) (または、Kernel#raise で受け入れられる引数に従わない他の引数) に変更すると、コードは SomeClass# である限り合格します。 some_method は StandardError または TypeError をレスキューします。

解決?

あなたが直面している制限(たとえば、「その特定の例外はテストでインスタンス化するのが難しい」など)を完全には理解していませんが、SomeError をインスタンス化することだけを強くお勧めしますが、単に継承する例外を作成するだけで目的を達成できます。 SomeError をモックとして使用します。

class SomeError < StandardError
  def initialize(some, random, params)
    # ...
  end
end

class MockSomeError < SomeError; def initialize(*);end; end  

class SomeClass
  def some_method
    mocked_method
    :ok
  rescue SomeError
    :ko
  end

  def mocked_method
    true
  end
end

describe SomeClass do
  subject(:some_class) { described_class.new }

  describe '#some_method' do
    subject(:some_method) { some_class.some_method }

    it { is_expected.to be :ok }

    context 'when #mocked_method fails' do
      before do
        allow(some_class).to receive(:mocked_method)
          .and_raise(MockSomeError)
      end

      it { is_expected.to be :ko }
    end
  end
end