ActiveRecordテストの通知アサーションをNotificationAssertionsヘルパーで統一
#57320に続くリファクタリングとして、activerecordテストスイート内に残っていた手動のActiveSupport::Notifications.subscribeパターンをNotificationAssertionsヘルパーへ移行した。対象はテストコードのみで、動作変更はない。
背景
ActiveSupport::Notificationsの手動サブスクリプションパターンには、テストコードを複雑にする固有の問題があった。コールバック内でフラグを立てて後から検証する方式(asserted = trueパターン)や、ensure節でのサブスクライバ解除忘れリスク、さらにコールバック内インラインアサーションによる可読性の低下が挙げられる。
#56989を起点にこの整理が始まり、trilogy_adapter_test.rbやinstrumentation_test.rb、query_cache_test.rbが#57320で移行済みだった。本PRはその続きとして、asynchronous_queries_test.rb、connection_pool_test.rb、relation/load_async_test.rbの3ファイルを対象とする。
技術的な変更
capture_notificationsが適用可能なケースでは、手動サブスクリプション全体をブロック呼び出しに置き換えることで、イベントの収集・解除・アサーションを宣言的に記述できる。
asynchronous_queries_test.rbのtest_async_query_foreground_fallbackでは、サブスクライバコールバック内でフラグを設定していた従来の方式が、capture_notificationsのブロックで包む形式に変わった。
変更前:
status = {}
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |event|
if event.payload[:sql] == "SELECT * FROM does_not_exists"
status[:executed] = true
status[:async] = event.payload[:async]
end
end
# ... テスト処理 ...
assert_equal true, status[:executed]
assert_equal false, status[:async]
ensure
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
変更後:
events = capture_notifications("sql.active_record") do
# ... テスト処理 ...
end
event = events.find { _1.payload[:sql] == "SELECT * FROM does_not_exists" }
assert_not_nil event
assert_equal false, event.payload[:async]
connection_pool_test.rbの2つのテストも同様に移行された。payloads配列への手動蓄積とensure節でのサブスクライバ解除が削除され、events.first.payloadへの直接アクセスに置き換えられている。ensure節のActiveSupport::Notifications.unsubscribe呼び出しも不要になり、コード量が削減された。
一方、load_async_test.rbではスレッド境界をまたぐイベント観測が必要なテストはActiveSupport::Notifications.subscribeを維持している。capture_notificationsが内部で使用するActiveSupport::Notifications.subscribedはスレッドローカルであるため、非同期クエリスレッドから発行されるsql.active_recordイベントを確実に捕捉できない。このファイルでの変更はstatus[:executed] = trueフラグの削除と、関連するアサーション文言の微修正にとどまる。
設計判断
適用範囲をスレッドセーフな場合に限定するという方針が明示的に採られている。capture_notificationsは利便性が高いが、スレッドローカルな購読メカニズムに基づく制約があり、全テストに一律適用することはできない。
PRではこのトレードオフを「安全である場合に限り移行する(In each case where it is safe)」と明記しており、load_async_test.rbの非同期スレッド観測テストを意図的に除外している。capture_notificationsを使える箇所と使えない箇所を明確に分離することで、テストの信頼性を損なわずにリファクタリングを進めている。
まとめ
本PRは、NotificationAssertionsヘルパーへの移行をActiveRecordテストスイート全体に段階的に適用するシリーズの一環だ。ensure節でのサブスクライバ管理という手続き的なコードが宣言的なブロック構文に置き換わることで、テストの意図が読み取りやすくなる。スレッドローカル制約を認識した上で適用範囲を選別している点は、テストインフラの設計理解を深める実例でもある。