Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use sorbet type checking with RSpec mocks?

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
like image 455
Rahil Shah Avatar asked Jun 24 '19 19:06

Rahil Shah


People also ask

How do I mock a method in RSpec?

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.

How does a mock work RSpec?

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.

How does Sorbet work Ruby?

Sorbet is multithreaded, scaling linearly across cores on your CPU. It checks your types in seconds, giving you feedback as you code.

How does a mock work in Ruby?

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.


2 Answers

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.

like image 172
jez Avatar answered Oct 21 '22 03:10

jez


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

like image 27
Jan Jedrychowski Avatar answered Oct 21 '22 02:10

Jan Jedrychowski