Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically define unique methods for each factory #1553

Open
georgebrock opened this issue Oct 31, 2022 · 0 comments
Open

Dynamically define unique methods for each factory #1553

georgebrock opened this issue Oct 31, 2022 · 0 comments
Labels

Comments

@georgebrock
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant