factory_bot
factory_bot copied to clipboard
Dynamically define unique methods for each factory
Problem this feature will solve
At GitHub we've recently adopted Sorbet for type annotations in our Ruby code, and we make extensive use of factory_bot in our test suite.
Both of these tools are great, but when using them together there's a problem: Sorbet can't statically determine the return type of factory_bot methods (#build
, #create
, etc.) because the return type depends on the arguments.
Desired solution
Dynamically defining a unique method for each strategy/factory combination would make it possible to provide type annotations, because the return type would now depend only on the method.
For example, instead of this:
thing = create(:thing, title: "Example")
We would be able to write this:
thing = create_thing(title: "Example")
I hacked together a little proof-of-concept, but it involves breaking encapsulation to get at the list of strategies:
module TypedFactoryMethods
def self.included(host)
# Egregious hack to get the list of strategies:
strategies = FactoryBot::Internal.strategies.instance_eval { @items.keys }
FactoryBot.factories.to_a.product(strategies).each do |factory, strategy|
define_factory_method(host, strategy, factory.name)
end
end
def self.define_factory_method(host, strategy, factory_name)
host.define_method("#{strategy}_#{factory_name}") do |*traits_and_overrides, &block|
FactoryBot::FactoryRunner.new(factory_name, strategy, traits_and_overrides).run(&block)
end
end
end
class Minitest::Test
include TypedFactoryMethods
end
This alone wouldn't be the only thing requires for Sorbet typed factories: we would also need a Tapioca DSL compiler to produce RBI files with the type information.
Alternatives considered
-
The lowest effort alternative would be for factory_bot to provide an approved way of getting the list of strategies, which would allow users of the gem to build this kind of thing for themselves without reaching too far into internals.
-
I tried various experiments with the Sorbet generics system before going down this path, but unfortunately it's not sufficiently expressive to capture ideas like "this method takes a class as an argument and returns an instance of that class."
Additional context
I'd be happy to open a PR here, but I figured it was worth opening an issue first to see if y'all were interested in this being a part of factory_bot first.
@georgebrock I'm also interested in using Sorbet and factory_bot together! Did you ever come to a solution here?