There's nothing wrong with the generated RBI file.
RSpec is very dependent on DSLs. When you call a method like RSpec.describe
, which accepts a block, RSpec
will execute that block in a specific scope with specific bindings, allowing you to call the RSpec testing DSL.
For Sorbet to gain knowledge of RSpec methods, it would need to be aware of this behaviour. There are ways in which you can hint Sorbet about the binding in which the code block received by a method will be executed. You can read about it on the Sorbet documentation.
If you look at the RBI file you mention, it doesn't include this information. Automatic RBI generators for gems usually limit themselves to establishing which constants (classes, modules) and which methods are present in a gem. This doesn't include a signature for those methods or their arguments.
If no hint as to the binding of a certain block, Sorbet assumes the block is bound to the block's outer context. For example, if you use RSpec.describe
on Ruby's toplevel, that's the context Sorbet will assume for the block passed to describe
. Since there's no definition of eq
on the global Ruby scope, the typechecking will fail.
To solve this there are many things you can do, with varying levels of effort and reward involved.
- Don't use Sorbet in your tests. Some people will argue tests don't need to be typed. After having had to refactor a lot of code in very big monolitic applications I strongly disagree with this but the truth is this is the most effortless solution. Simply put
# typed: ignore
at the beggining of all your spec files and move on. You'll miss out on all the benefits of static typing in your test suite.
- Create a shim hinting Sorbet that the block passed to
RSpec.describe
should be bound to T.untyped
. I've managed to use this solution in the past successfully. You will not get all the benefits of Sorbet, specially on the RSpec methods, but at least you will get typechecking when calling your own code. See below for an example of what this would look like. This will allow you to use # typed: true
on your spec files.
- Create a shim binding every method in RSpec to the correct class. This would take a long time and no doubt be a really big effort but if you managed to do this, you'd get almost all the benefits of using Sorbet with RSpec. And you could share it with the community. This might also require writing code for a Tapioca compiler that generates appropriate types for things defined using
let(:something)
.
I don't believe in 1 and 3 is a lot of work so now I will detail how to proceed for the second option.
Create a shim file for rspec in sorbet/rbi/shims/rspec.rbi
like this one:
# typed: strict
module RSpec
end
In this file we will add the necessary signatures to not assume anything about the binding of the blocks passed to RSpec methods. For example, let's start with describe
. If you go to the autogenerated RBI, you'll see it has this declaration:
def self.describe(*args, &example_group_block); end
Let's copy this to our own RBI file and prepend our own signature.
# typed: strict
module RSpec
sig do
params(
args: T.untyped,
example_group_block: T.proc.bind(T.untyped).void
).void
end
def self.describe(*args, &example_group_block); end
end
We've now told Sorbet that the block you pass to describe
is bound to T.untyped
. This type acts as the escape hatch, allowing any method calls, so Sorbet will not complain about missing methods inside it. You might need to repeat this operation for other methods even in different RSpec classes until you can fully convince the typechecker to leave you alone.
With this you should be able to use # typed: true
in your spec files. It's not perfect, actually it's nothing more than a patch, but it will at least grant you some very basic typechecking capabilities.
# typed: false
. Also, few additions: usetapioca
gem to create type definitions for your gems andrspec-sorbet
which allows to use doubles with typeed checked code. – Hyalo