thoughtbot / thoughtbot/factory_bot

Dynamically define unique methods for each factory

Open
#1,553 1 comment 5 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

feature
Dominant language
Ruby
Stars
8.2k
Forks
2.6k
PR merge metrics
No merged PRs in 30d

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the FactoryBot::Internal.strategies, FactoryBot.factories, and FactoryBot::FactoryRunner entry points shown in the proof of concept. Determine the supported strategy and factory combinations, then add coverage for dynamically generated methods and their argument forwarding. Done means callers can use unique strategy/factory methods without reaching into internals.

Written by the indexing model from the issue text.

Assessment

Tech stack
ruby
Domain
testing-qa, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.