`factory`キーとブロックの同時指定を `method_missing` 経由でも正しくエラーにする

thoughtbot/factory_bot

DefinitionProxy#method_missing がアソシエーション定義時にブロックを #association へ転送しておらず、factoryキーとブロックを同時に渡してもエラーが発生しないという問題が修正されました。1文字の追加で既存のバリデーションが正しく機能するようになります。

背景

method_missing を介したアソシエーション定義でブロックが黙って無視される問題が #1503 で報告されていました。たとえば author(factory: :user) { :some_block } のように記述しても、ブロックは捨てられ、エラーも発生しないままアソシエーションが定義されます。ユーザーはブロックが有効だと思い込み、意図しない動作に気づきにくい状況でした。

この問題への対応として #1518 が作成されましたが、クローズされたため、本PR #1579 がその代替として提出されました。

技術的な変更

DefinitionProxy#method_missingassociation 呼び出しに &block を追加するだけの1行変更です。

変更前:

association(name, association_options)

変更後:

association(name, association_options, &block)

#association メソッドにはすでにブロック付き呼び出しを検出して AssociationDefinitionError を発生させる実装が存在します。method_missing 側でブロックを転送していなかったため、そのバリデーションがバイパスされていました。転送を追加することで既存のエラーチェックが正しく機能するようになります。

スペックでは以下のシナリオが追加されています:

it "raises an AssociationDefinitionError when called with a `:factory`-key and providing a block" do
  definition = FactoryBot::Definition.new(:user)
  proxy = FactoryBot::DefinitionProxy.new(definition)
  invalid_call = lambda do
    proxy.author(factory: :user) { :this_should_raise_an_error }
  end

  expect(invalid_call).to raise_error(
    FactoryBot::AssociationDefinitionError,
    "Unexpected block passed to 'author' association in 'user' factory"
  )
end

このテストにより、method_missing 経由でも #association 直接呼び出しと同等のバリデーションが行われることが保証されます。

設計判断

ブロックを無視してサイレントに処理を続けるのではなく、エラーを明示的に発生させる方針が選択されています。PR本文にも「このふるまいはユーザーが予期しない可能性があるため、渡されたブロックが無視される旨のエラーを発生させるべき」と明記されています。

また、PR #1518 のコメントでは破壊的変更の可能性も議論されていましたが、#association 側のバリデーション実装はすでに存在しており、本PRは「バリデーションのバイパスを塞ぐ」という位置づけです。新たな制約を加えるのではなく、意図したバリデーションが正しく適用されるよう経路を修正した変更といえます。

まとめ

1文字の追加(&block)によって、method_missing 経由のアソシエーション定義と #association 直接呼び出しの動作が統一されました。factoryキーとブロックを同時に指定した際の意図せぬサイレント無視が解消され、明示的なエラーでユーザーに誤りを通知できるようになります。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
1ea04c0d

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術詳細・設計判断(各論)、まとめ(結論)という「総論→各論→結論」の構成が明確に適用されており、模範的です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:path/to/file.rb)と、PR/Issue番号のリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

factory_botの内部実装に関するトピックであり、専門用語も適切に使用されているため、対象読者であるエンジニアに適した内容です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが要約から始まる構成であり、各段落もトピックセンテンスで始まっているため、非常に高い可読性を確保しています。1段落1トピックの原則も守られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

変更前後のコード、追加されたテストコード、そしてファイル名が、提供されたDiff情報と完全に一致しており、正確に引用されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`DefinitionProxy`、`method_missing`、`AssociationDefinitionError`といった技術用語が文脈に沿って正しく使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

変更が既存のバリデーションロジックへの経路を有効化するものであるという説明は、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのDescription、Diff、および参照されている関連PRの情報で裏付けられており、ハルシネーションは認められません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#1579, #1518)やIssue番号(#1503)などの固有名詞はすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルは、PRの技術的な実装内容だけでなく、それがもたらすユーザー視点での結果を要約しており、PRの主題を的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に含まれないバージョンサポート状況やリリース日程などの外部知識は記載されておらず、事実に忠実な内容です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

PRがマージされた過去の変更として「〜が修正されました」「〜報告されていました」といった時間表現が正しく使われています。