How to use sorbet type checking with RSpec mocks?
Solution 1:
Use instance_double
with a proper class and mock it's is_a?
. To do that globally perform monkey-patching:
require 'rspec/mocks'
class RSpec::Mocks::InstanceVerifyingDouble
def is_a?(expected)
@doubled_module.target <= expected || super
end
end
Solution 2:
Selectively, do not raise exception when caused by mocks. This way Sorbet still performs types checks unless a mock is used.
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
Mocha mocks (stub in tests) will not pass any type checks by default. This is deliberate and considered a feature; bare mocks make tests brittle and tend to cause problems when refactoring code, regardless of type checking.
When trying to test a method using a Mocha mock that fails a type check, we recommend rewriting the test to not use Mocha mocks. Either:
- Create a genuine instance of the object, and use
.stubs
to replace only certain methods. - Write helper functions to create real instances of your objects with fake data.
In the worst case, you can stub is_a?
to make a Mocha mock pass a type check, but please avoid doing this. It results in brittle tests and makes code harder to reason about. If you must:
# NOT RECOMMENDED!
fake_llama = stub
fake_llama.stubs(:llama_count).returns(17)
fake_llama.stubs(:is_a?).with(M::Llama).returns(true)
I'm not familiar with the differences between RSpec's mocks and Mocha's mocks (at Stripe where Sorbet is developed we use Mocha) but the principles should be the same.