How to use sorbet type checking with RSpec mocks?
Asked Answered
B

2

14

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
Basia answered 24/6, 2019 at 19:9 Comment(0)
B
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.

Bedel answered 24/6, 2019 at 20:56 Comment(2)
Got it! Thanks. I refactored the test code as you suggested and it works fine now.Basia
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: github.com/rspec/rspec-mocks/issues/1286Chancellorship
V
9

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

Variety answered 5/7, 2019 at 14:46 Comment(1)
The above is done in this gem: github.com/tricycle/rspec-sorbetAristocracy
B
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.

Bedel answered 24/6, 2019 at 20:56 Comment(2)
Got it! Thanks. I refactored the test code as you suggested and it works fine now.Basia
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: github.com/rspec/rspec-mocks/issues/1286Chancellorship

© 2022 - 2024 — McMap. All rights reserved.