RSpec:为什么“instance_double”适用于 StandardError,而不适用于其他异常类?

RSpec: Why does `instance_double` work with StandardError, but not other exception classes?

提问人:Richard-Degenne 提问时间:8/10/2023 更新时间:8/10/2023 访问量:103

问:

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

下面是一个示例。

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

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

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 时是否存在一些边缘情况?或者,有没有更好的方法来模拟难以实例化的异常类?

Ruby 异常 rspec 模拟

评论


答:

3赞 TonyArra 8/10/2023 #1

该消息实际上来自宝石。它检查引发的错误是否通过检查。实例 double 不会对检查做出 true 响应。TypeErrorpryexception.is_a?(Exception)is_a?

更常见的是引发错误的实例,其中包含一些参数值:

before do
  allow(some_class).to receive(:mocked_method)
    .and_raise(SomeError.new(1, 'a', 'b'))
end

评论

0赞 Richard-Degenne 8/10/2023
我的项目不使用 .而且,如问题所述,在测试环境中很难生成实例化误差所需的参数。pry
5赞 engineersmnky 8/10/2023 #2

问题解释

这是由 Kernel#raise 引起的。TypeError

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

Kernel#raise接受以下内容:

  • 没有参数 - “在 $!或者如果 $!nil,则引发 RuntimeError;或
  • A String - “引发一个 RuntimeError,并将字符串作为消息。
  • "一个 Exception 或其他对象,该对象在发送异常消息时返回 Exception 对象“ - 将引发此问题Exception

在您的情况下,以上都不是,因此会抛出一个.同样可以复制:instance_double(SomeError)Kernel#raiseTypeError

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

掩人耳目

原因有效不是你想的那样。StandardError

相反,看起来只是工作,因为正在救援并且是一个(继承自 )。在这种情况下,它仍在被提出,它只是在这个过程中被拯救。StandardErrorSomeClass#some_methodStandardErrorTypeErrorStandardErrorStandardErrorTypeError

您可以通过更改为 (或任何其他不遵守接受的参数的参数) 来证明这一点,只要 rescues 或 ,代码仍然会通过。and_raise(instance_double(StandardError))and_raise(instance_double(SomeError))Kernel#raiseSomeClass#some_methodStandardErrorTypeError

溶液?

虽然我不完全理解您受到的限制(例如“该特定异常很难在测试中实例化”),并且完全建议只实例化一个 ,您可以通过简单地创建一个继承自 的异常来实现您的目标作为模拟。SomeErrorSomeError

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

评论

0赞 Richard-Degenne 8/10/2023
谢谢,这更有意义!有趣的是,您提供的解决方案正是我想出的解决方法,尽管在帮助程序中定义了一个匿名类。let
0赞 Richard-Degenne 8/10/2023
为了进一步的细节,我在这里尝试模拟的错误类是 Gitlab::Error:ResponseError。它的构造函数需要 ,而 又需要 和 ,而 和 本身需要更多的参数,依此类推。HTTParty::ResponseHTTParty::RequestNet::HTTPResponse