Skip to content

Don't require a custom strategy to implement #to_sym - #1839

Open
IslamElsayed wants to merge 2 commits into
thoughtbot:mainfrom
IslamElsayed:custom-strategy-association
Open

IslamElsayed wants to merge 2 commits into
thoughtbot:mainfrom
IslamElsayed:custom-strategy-association

Conversation

@IslamElsayed

Copy link
Copy Markdown

Fixes #1536.

The bug

With use_parent_strategy on (the default), building a factory that has an association calls @build_strategy.to_sym unconditionally:

strategy_override = overrides.fetch(:strategy) {
  FactoryBot.use_parent_strategy ? @build_strategy.to_sym : :create
}

A strategy only has to implement #result and #association — #to_sym is something the five built-in strategies happen to have. Any custom strategy without it raises:

NoMethodError: undefined method `to_sym' for an instance of #<Class:0x...>

@composerinteralia called this an unintentional breaking change on the issue and wrote out the fix; it was never implemented, and the only PR on the thread (#1537) was docs-only.

The fix

His suggestion, falling back to the strategy's class. That works because Strategy.lookup_strategy already returns a Class unchanged:

def self.lookup_strategy(name_or_object)
  return name_or_object if name_or_object.is_a?(Class)

  FactoryBot::Internal.strategy_by_name(name_or_object)
end

so the association is built with the same custom strategy as its parent — which is what use_parent_strategy is asking for.

One deliberate deviation: I inlined the condition rather than extracting it into a private method. Evaluator defines a factory's attributes as singleton methods on itself, so adding an instance method risks shadowing an attribute that happens to share its name.

Testing

An acceptance example in register_strategies_spec.rb registering a strategy that implements #result and #association but not #to_sym, and asserting the association is built. Before the fix it fails with exactly the NoMethodError from the issue.

I checked it isn't vacuous by breaking the fix two ways: restoring the unconditional to_sym fails it, and returning :create from the fallback instead of the class also fails it — so it pins that the association uses the custom strategy, not merely that nothing raises.

Full suite: 775 examples, 0 failures. standardrb clean on both files. Run under Ruby 3.3.5 rather than the 4.0.1 in .tool-versions, since the gemspec allows >= 3.0.0.

I have not added the deprecation @composerinteralia mentioned for eventually removing the fallback — happy to if you want it in the same PR.

Disclosure: I use AI assistance when working on patches, and I check and test everything before submitting.

With `use_parent_strategy` on, building a factory that has an association
called `@build_strategy.to_sym` unconditionally to decide which strategy
the association should use. A strategy only has to implement `#result` and
`#association`; `#to_sym` is something the five built-in strategies happen
to have, so any custom strategy without it raised:

    NoMethodError: undefined method `to_sym' for an instance of #<Class:...>

Fall back to the strategy's class, which is what @composerinteralia
suggested on thoughtbot#1536. `Strategy.lookup_strategy` already returns a Class
unchanged, so the association is built with the same custom strategy as
its parent, which is what `use_parent_strategy` means.

The condition is inlined rather than extracted into a method because
`Evaluator` defines a factory's attributes as singleton methods on itself,
so a new instance method could shadow an attribute of the same name.

Closes thoughtbot#1536

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NoMethodError: undefined method 'to_sym' for CustomStrategy

1 participant