RSpec:为什么 `instance_double` 可以与 StandardError 一起使用,但不能与其他异常类一起使用?

Ric*_*nne 4 ruby rspec exception mocking

在某些测试中,我想设置一个引发特定异常类的模拟。因为该特定异常很难在测试中实例化,所以我想使用双精度。

这是一个例子。

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
Run Code Online (Sandbox Code Playgroud)

但是,运行此测试失败并显示以下消息。

     Failure/Error:
       mocked_method

     TypeError:
       exception class/object expected
Run Code Online (Sandbox Code Playgroud)

奇怪的是,如果我替换SomeErrorStandardError,它就可以正常工作。

     Failure/Error:
       mocked_method

     TypeError:
       exception class/object expected
Run Code Online (Sandbox Code Playgroud)

这里发生了什么?嘲笑时是否有一些边缘情况StandardError?或者,是否有更好的方法来模拟难以实例化的异常类?

eng*_*nky 5

问题说明

TypeError原因是Kernel#raise.

RSpec::Mocks::MessageExpectation#and_raise将调用包装在稍后调用的Kernel#raisea Proc(如图所示)中。

Kernel#raise接受以下内容:

  • 无参数 - “引发异常$!或引发RuntimeErrorif $!is nil;或者
  • 字符串 - “将RuntimeError字符串作为消息引发。” ; 或者
  • 发送消息时返回对象的一个Exception​​或另一个对象”Exceptionexception -Exception将引发此问题

在您的情况下,instance_double(SomeError)以上都不是,因此Kernel#raise会抛出TypeError. 同样的内容可以通过以下方式重现:

raise({a: 12})
in `raise': exception class/object expected (TypeError)
Run Code Online (Sandbox Code Playgroud)

红鲱鱼

起作用的原因StandardError并不是你想象的那样。

相反,StandardError仅仅看起来有效,因为SomeClass#some_method正在救援StandardError并且TypeErrorStandardError(继承自StandardError)。在这种情况下,它TypeError仍在被提升,它只是在这个过程中被营救。

您可以通过更改and_raise(instance_double(StandardError))and_raise(instance_double(SomeError))(或任何其他不符合 接受的参数的参数Kernel#raise)来证明这一点,并且只要SomeClass#some_method救援StandardError或 ,代码仍然会通过TypeError

解决方案?

虽然我不完全理解您所面临的限制(例如“该特定异常很难在测试中实例化”),并且完全建议仅实例化 a ,但您可以通过简单地创建一个继承自asSomeError的 Exception 来实现您的目标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
Run Code Online (Sandbox Code Playgroud)

例子