ネストしたファクトリのデバッグを支援する `factory_bot.before_run_factory` イベント

thoughtbot/factory_bot

factory_botに factory_bot.before_run_factory という新しい ActiveSupport::Notifications イベントが追加されました。これにより、深くネストしたファクトリの呼び出しスタックをランタイムで構築できるようになり、after コールバックで発生するエラーの追跡が大幅に容易になります。

背景

ネストした関連を持つファクトリでエラーが発生した場合、エラーメッセージにはトップレベルのファクトリ名しか現れず、どのファクトリチェーンがエラーに至ったかを追跡することが困難でした。これが #1673 で報告された問題の本質です。

この問題を解決するための既存手段はいずれも限界を抱えていました。グローバルコールバックはコールバック内からファクトリ名を取得できず、factory_bot.compile_factory イベントはファクトリ定義のキャッシュにより複数のスペック実行時には再発火しません。run_factory イベントの start/finish サブスクライバを使う方法は技術的には機能しますが、ほとんどのユーザーが気づかない難解なパターンでした。

これらの代替手段がいずれも不十分であったため、ファクトリ実行直前に発火する専用のインストゥルメンテーションイベントが必要とされていました。

技術的な変更

lib/factory_bot/factory_runner.rb に1行を追加することで、既存の run_factory イベントの直前に新しいイベントが発火するようになりました。

変更前:

ActiveSupport::Notifications.instrument("factory_bot.run_factory", instrumentation_payload) do
  factory.run(runner_strategy, @overrides, &block)
end

変更後:

ActiveSupport::Notifications.instrument("factory_bot.before_run_factory", instrumentation_payload)

ActiveSupport::Notifications.instrument("factory_bot.run_factory", instrumentation_payload) do
  factory.run(runner_strategy, @overrides, &block)
end

新しい instrument 呼び出しはブロックを持たない非ブロック形式であることに注目してください。run_factory はブロックを渡してファクトリの実行全体を計測するのに対し、before_run_factory はファクトリ実行の開始を通知するだけなので、ブロックを持たない単発の発火として実装されています。

ペイロードは既存の instrumentation_payload を再利用しており、:name:strategy:traits:overrides:factory という run_factory と同一のキーが提供されます。新たなペイロード構築のコードは一切不要です。

設計判断

before_run_factoryrun_factory をプッシュ/ポップのペアとして使う設計が採用されました。

ドキュメントとテストで示されているユースケースは、配列によるコールスタックの構築です。before_run_factory でファクトリ名をスタックに積み、run_factory の完了時にポップすることで、任意のタイミングにおけるファクトリのネスト状態を把握できます。

factory_call_stack = []

ActiveSupport::Notifications.subscribe("factory_bot.before_run_factory") do |name, start, finish, id, payload|
  factory_call_stack.push(payload[:name])
end

ActiveSupport::Notifications.subscribe("factory_bot.run_factory") do |name, start, finish, id, payload|
  factory_call_stack.pop
end

run_factory のブロック計測モデル(開始時に start、終了時に finish が発火する)と組み合わせることで、before_run_factorystart イベントと run_factoryfinish イベントを対応させるより、このプッシュ/ポップパターンのほうがサブスクライバの実装をシンプルに保てます。また、ペイロードを新規に設計せず既存の instrumentation_payload を流用することで、変更範囲が最小限に抑えられています。

まとめ

本PRは factory_runner.rb への1行追加という最小限の変更で、深くネストしたファクトリのデバッグという長年の課題に対処しています。既存のインストゥルメンテーション基盤と同一のペイロードを再利用する設計により、ユーザーは新しいAPIを学ぶことなく、使い慣れた ActiveSupport::Notifications のパターンでファクトリ呼び出しスタックを構築できます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
6a4baebd

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

「総論→各論→結論」の構成が明確で、リード文、背景、技術詳細、設計判断、まとめの各要素が適切に配置されており、非常に分かりやすい記事構成です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライトやGitHubのPR/Issueへのリンク記法がガイドラインに沿って正しく使われており、技術記事として体裁が整っています。

対象読者への適合性 ✓ PASS

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

専門用語を適切に使い、冗長な説明を省くことで、対象読者であるエンジニアにとって簡潔で読みやすい内容になっています。

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

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

各セクション・各パラグラフが「総論→各論」の構造で書かれており、トピックセンテンスが明確なため、記事の骨子を素早く掴むことができます。

Diff内容との照合 ✓ PASS

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

`lib/factory_bot/factory_runner.rb`のコード変更点や、ドキュメントに追記されたコード例が、提供されたDiff情報と正確に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`ActiveSupport::Notifications`や`instrumentation_payload`など、関連する技術用語が文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

新しいイベントがブロックを持たない形式である点や、既存のペイロードを再利用している点など、技術的な説明が正確かつ論理的です。

事実の突合 ✓ PASS

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

PRのDescriptionで言及されている「既存の代替手段の限界」や「プッシュ/ポップ設計」などが、記事内で正確に事実として説明されており、ハルシネーションは見られません。

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

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

PR番号(#1795)やIssue番号(#1673)、イベント名など、記事に含まれる固有名詞や数値はすべて正確です。

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

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

記事のタイトルがPRの主題を的確に要約しつつ、変更の目的(デバッグ支援)を補足しており、読者の関心を引く優れたタイトルになっています。

外部知識の正確性 ✓ PASS

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

PR情報に記載のないバージョン情報やリリース予定などの外部知識を含んでおらず、信頼性が高い記事です。

時間表現の正確性 ✓ PASS

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

記事内の時間表現はPRで導入された変更の現状を正しく記述しており、時間的な歪曲はありません。