I have a method that that has a sorbet type signature definition. While trying to mock this method in tests using RSpec I get a type mismatch error. I'm trying to understand how I can resolve this issue and can add RSpec based tests without affecting sorbet type check.
sig {params(login_context: LoginContext, company_id: String).returns(T::Boolean)}
def populate_dummy_data(login_context, company_id)
Test Code:
@login_context = double(LoginContext, :requester => @requester) # Creates an instance of type Rspec::Mocks::double
Error:
expected no Exception, got #<TypeError: Parameter ‘login_context’: Expected type LoginContext, got type RSpec::Mocks::Double wit...a_populator_spec.rb:42
Mocking with RSpec is done with the rspec-mocks gem. If you have rspec as a dependency in your Gemfile , you already have rspec-mocks available.
Generally speaking, a mock is a replica or imitation of something. RSpec mocks, in the same sense, are an imitation of return values or method implementations. The ability to carry out this kind of imitation makes it possible to set expectations that specific messages are received by an object.
Sorbet is multithreaded, scaling linearly across cores on your CPU. It checks your types in seconds, giving you feedback as you code.
Mocks are a handy tool for writing tests in Ruby. You can use them to fake an object and verify that the correct methods were called against it. Perfect for testing a method that integrates closely with another class or module.
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:
.stubs
to replace only certain methods.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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With