ActiveRecordテストの通知サブスクライバ手動実装を `NotificationAssertions` ヘルパーに統一

rails/rails

ActiveSupport::Notifications.subscribed を使った手動サブスクライバパターンを、NotificationAssertions ヘルパーに置き換えることで、ActiveRecordのテストコードが簡潔になりました。#53822 で始まったリファクタリングの続編にあたります。

背景

ActiveSupport::Testing::NotificationAssertions は、通知イベントの捕捉・検証を宣言的に行えるテストヘルパーモジュールです。従来、ActiveRecordのテストでは ActiveSupport::Notifications.subscribed にラムダを渡し、ローカル配列にイベントを蓄積するパターンが各所に散在していました。このパターンは機能的に問題はないものの、コールバック定義・ブロック・蓄積変数が混在してテストの意図が読み取りにくくなっていました。

#53822 がActiveRecordの主要テストに NotificationAssertions を適用した初回のリファクタリングPRであり、本PR #56989 はその後継として、対応が残っていた instrumentation_test.rbload_async_test.rb の計6テストに同様の置き換えを適用しています。

技術的な変更

変更は大きく2つのパターンに分類される。ペイロードの内容を検査する必要があるケースには capture_notifications を、通知が発火したかどうかだけを確認するケースには assert_notification を使用しています。

capture_notifications + map/reject への置き換え

instrumentation_test.rbtest_payload_affected_rows では、ブロック内でイベントを蓄積していた手動サブスクライバが capture_notifications に置き換えられました。

変更前:

affected_row_values = []

ActiveSupport::Notifications.subscribed(
  -> (event) { affected_row_values << event.payload[:affected_rows] },
  "sql.active_record",
) do
  Book.insert_all!([...], returning: false)
  # ...
end

変更後:

events = capture_notifications("sql.active_record") do
  Book.insert_all!([...], returning: false)
  # ...
end
affected_row_values = events.map { |e| e.payload[:affected_rows] }

capture_notifications はブロック内で発火した全イベントを配列として返すため、ペイロードの抽出ロジックをブロックの外に分離できます。コールバック定義と蓄積変数の初期化が不要になり、テストの「何を検証しているか」が前面に出る構造になっています。

フィルタリングの外部化

test_payload_affected_rows_cascade では、コールバック内で SCHEMATRANSACTION イベントを除外していた条件分岐が、capture_notifications のブロック外の reject に移動されています。

変更前:

ActiveSupport::Notifications.subscribed(
  -> (event) do
    unless event.payload[:name].in? ["SCHEMA", "TRANSACTION"]
      affected_row_values << event.payload[:affected_rows]
    end
  end,
  "sql.active_record",
) do
  # ...
end

変更後:

events = capture_notifications("sql.active_record") do
  # ...
end
affected_row_values = events
  .reject { |e| e.payload[:name].in?(["SCHEMA", "TRANSACTION"]) }
  .map { |e| e.payload[:affected_rows] }

load_async_test.rb でも同様に、SCHEMA除外の条件がコールバック内から reject チェーンに移動されています。フィルタリングロジックが通知の購読から切り離されることで、どのイベントを対象にしているかが一目で把握できます。

assert_notification による存在確認の簡略化

test_load_async_instrumentation_is_thread_safe では、通知が発火したかどうかのフラグ管理が assert_notification に置き換えられました。

変更前:

notification_called = false
ActiveSupport::Notifications.subscribed(->(*) { notification_called = true }, "sql.active_record") do
  Post.count
end

assert(notification_called)

変更後:

assert_notification("sql.active_record") do
  Post.count
end

フラグ変数・サブスクライバ・後続アサーションの3点セットが、assert_notification の1ブロックに集約されています。

設計判断

capture_notificationsassert_notification を使い分ける アプローチが採用されています。ペイロードの具体的な値を検証する必要がある場合は capture_notifications でイベントを一括取得してから map/reject で後処理し、発火の有無だけを確認する場合は assert_notification を使うという明確な使い分けがPR本文に示されています。

この設計は、テストコードの意図をヘルパーの選択で表現するものです。assert_notification を使ったテストは「このブロックで通知が発火すること」を主張しており、capture_notifications を使ったテストは「発火した通知のペイロードを検証すること」を主張しています。ヘルパーの種類がそのままテストの目的を示すドキュメントになっています。

なお、フィルタリング処理をコールバック内からブロック外の reject に移したことで、全イベントが一度キャプチャされてから後処理される動作に変わっています。テストの検証結果は同等ですが、コールバック内での条件分岐という副作用的な実装が排除され、データ変換のパイプラインとして明示的に表現されています。

まとめ

本PRは ActiveSupport::Notifications.subscribed の手動パターンを NotificationAssertions ヘルパーに統一することで、通知のキャプチャ・フィルタリング・アサーションの各ステップを明確に分離しました。コールバック定義・ローカル変数・購読ブロックが入り組んだコードが、目的を宣言するヘルパー呼び出しに置き換わったことで、テストが何を検証しているかを読み解くコストが大幅に下がっています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
185c8edb

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→セクション群(各論)→まとめ(結論)の3部構成が明確に適用されています。背景、技術的な変更、設計判断の各セクションが適切に配置されており、記事の流れが非常に論理的です。

カスタムMarkdown構文 ✓ PASS

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

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

対象読者への適合性 ✓ PASS

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

ActiveRecordのテストコードやActiveSupport::Notificationsに関する知識を前提としており、専門知識を持つエンジニアという対象読者に完全に適合しています。

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

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

各セクションが総論→各論の構成になっており、各段落の先頭にトピックセンテンスが配置されているため、非常に読みやすいです。1段落1トピックの原則も守られています。

Diff内容との照合 ✓ PASS

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

記事内のすべてのコードブロックは、提供されたDiff情報と正確に一致しています。コードの一部省略([...])も、文脈を損なわない範囲で適切に行われています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`NotificationAssertions`, `capture_notifications`, `assert_notification` などの技術用語が、PRの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

コード変更の意図(手動サブスクライバからヘルパーへの置き換え)や、各ヘルパーの役割についての説明が技術的に正確かつ明快です。

事実の突合 ✓ PASS

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

記事内のすべての主張(先行PRの存在、変更対象のファイル、変更されたテストの数など)は、PRのDescriptionやDiffの内容によって裏付けられています。ハルシネーションは見られません。

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

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

PR番号(#56989, #53822)や変更されたテストの数(計6テスト)など、すべての数値と固有名詞が正確です。

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

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

記事のタイトルはPRの主題「Simplify `activerecord` tests with `NotificationAssertions` helpers」を的確に日本語で表現しており、内容と完全に一致しています。

外部知識の正確性 ✓ PASS

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

PR情報に含まれない外部知識(バージョンのサポート状況、リリース日程など)の追記はなく、提供された情報に忠実です。

時間表現の正確性 ✓ PASS

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

「続編にあたります」や「従来」といった時間表現が、PRの「Follow-up」という背景と正しく整合しています。