如何通过 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
我有一个具有冰糕类型签名定义的方法。 在使用 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