factory_bot icon indicating copy to clipboard operation
factory_bot copied to clipboard

Dynamically define unique methods for each factory

Open georgebrock opened this issue 1 year ago • 1 comments

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 avatar Oct 31 '22 15:10 georgebrock

@georgebrock I'm also interested in using Sorbet and factory_bot together! Did you ever come to a solution here?

nathanmsmith avatar May 05 '24 21:05 nathanmsmith