如何通过 RSpec 模拟使用 sorbet 类型检查?

How to use sorbet type checking with RSpec mocks?

我有一个具有冰糕类型签名定义的方法。 在使用 RSpec 的测试中尝试模拟此方法时,出现类型不匹配错误。 我正在尝试了解如何解决此问题并添加基于 RSpec 的测试而不影响冰糕类型检查。

sig {params(login_context: LoginContext, company_id: String).returns(T::Boolean)}
  def populate_dummy_data(login_context, company_id)

测试代码:

@login_context = double(LoginContext, :requester => @requester) # Creates an instance of type Rspec::Mocks::double

错误:

expected no Exception, got #<TypeError: Parameter ‘login_context’: Expected type LoginContext, got type RSpec::Mocks::Double wit...a_populator_spec.rb:42

默认情况下,Mocha 模拟(测试中的存根)不会通过任何类型检查。这是故意的,被认为是一个功能;裸模拟使测试变得脆弱,并且在重构代码时往往会导致问题,无论类型检查如何。

当尝试使用未通过类型检查的 Mocha mock 测试方法时,我们建议重写测试以不使用 Mocha mock。或者:

  • 创建对象的真实实例,并使用.stubs仅替换某些方法。
  • 编写辅助函数以使用假数据创建对象的真实实例。

在最坏的情况下,您可以存根 is_a? 以使 Mocha mock 通过类型检查,但请避免这样做。它会导致脆弱的测试并使代码更难推理。如果你必须:

# NOT RECOMMENDED!

fake_llama = stub
fake_llama.stubs(:llama_count).returns(17)
fake_llama.stubs(:is_a?).with(M::Llama).returns(true)

我不熟悉 RSpec 的 mock 和 Mocha 的 mock 之间的区别(在开发 Sorbet 的 Stripe,我们使用 Mocha),但原理应该是相同的。

解决方案 1:

使用 instance_double 和适当的 class 并模拟它是 is_a?。要在全球范围内执行 monkey-patching:

require 'rspec/mocks'

class RSpec::Mocks::InstanceVerifyingDouble
  def is_a?(expected)
    @doubled_module.target <= expected || super
  end
end

解决方案 2:

有选择地,当由模拟引起时不引发异常。 这样 Sorbet 仍然执行类型检查,除非使用模拟。

require 'sorbet-runtime'

RSpec.configure do |config|
  config.before :each, sorbet: :mocks do
    T::Configuration.inline_type_error_handler = proc do |error|
      raise error unless error.message.include? "got type RSpec::Mocks"
    end

    T::Configuration.call_validation_error_handler = proc do |_signature, opts|
      raise TypeError.new(opts[:pretty_message]) unless opts[:message].include? "got type RSpec::Mocks"
    end
  end


  config.after :each, sorbet: :mocks do
    T::Configuration.inline_type_error_handler = nil
    T::Configuration.call_validation_error_handler = nil
  end
end