12

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
Rahil Shah
  • 183
  • 1
  • 4

2 Answers2

8

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.

jez
  • 1,239
  • 1
  • 7
  • 14
  • Got it! Thanks. I refactored the test code as you suggested and it works fine now. – Rahil Shah Jun 25 '19 at 08:07
  • 3
    I'm not convinced this justification is wholly appropriate for RSpec mocks, as (I believe) they have some different properties to mocha. Have created an issue to discuss: https://github.com/rspec/rspec-mocks/issues/1286 – Xavier Shay Jul 29 '19 at 23:55
8

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

Michael Gall
  • 183
  • 7