キャッシュストアのInstrumentationテストを`NotificationAssertions`で簡潔化
activesupport/test/cache/behaviors/cache_store_behavior.rb 内の2つのInstrumentationテストが、ActiveSupport::Testing::NotificationAssertions の capture_notifications ヘルパーを使う形に書き換えられました。これにより、手動のサブスクライバー管理と ensure ブロックによるクリーンアップが不要になります。
背景
ActiveSupport::Testing::NotificationAssertions は #53065 と #54126 で導入されたテストヘルパーモジュールです。従来、ActiveSupport::Notifications のイベント発火を検証するには、Notifications.subscribe で手動サブスクライバーを登録し、テスト終了後に ensure ブロックで Notifications.unsubscribe を呼ぶパターンが必要でした。
#53824 でこのヘルパーへの移行が activesupport テストの一部で始まり、今回の #56974 はそのフォローアップとして cache_store_behavior.rb の残余テストを対象にしています。同時期に actionpack 向けの #56956 や activerecord 向けの #56975 も進行しており、Rails全体でこのヘルパーへの統一が段階的に進められています。
cache_store_behavior.rb はbehaviorモジュールとして、file_store_test.rb・mem_cache_store_test.rb・memory_store_test.rb・redis_cache_store_test.rb にincludeされるため、ここでの変更は複数のキャッシュストア実装のテスト全体に波及します。
技術的な変更
test_cache_hit_instrumentation と test_cache_miss_instrumentation の2メソッドが、capture_notifications を使う形にリファクタリングされました。変更の本質は、イベントキャプチャのスコープをブロックで明示的に囲む点にあります。
変更前(test_cache_hit_instrumentation):
def test_cache_hit_instrumentation
key = "test_key"
@events = []
ActiveSupport::Notifications.subscribe("cache_read.active_support") { |event| @events << event }
assert @cache.write(key, "1", raw: true)
assert @cache.fetch(key, raw: true) { }
assert_equal 1, @events.length
assert_equal "cache_read.active_support", @events[0].name
assert_equal :fetch, @events[0].payload[:super_operation]
assert @events[0].payload[:hit]
ensure
ActiveSupport::Notifications.unsubscribe "cache_read.active_support"
end
変更後(test_cache_hit_instrumentation):
def test_cache_hit_instrumentation
key = "test_key"
assert @cache.write(key, "1", raw: true)
events = capture_notifications("cache_read.active_support") do
assert @cache.fetch(key, raw: true) { }
end
assert_equal 1, events.length
assert_equal "cache_read.active_support", events[0].name
assert_equal :fetch, events[0].payload[:super_operation]
assert events[0].payload[:hit]
end
test_cache_miss_instrumentation も同様に書き換えられています。正規表現パターン /^cache_(.*)\.active_support$/ を使ったサブスクライバーも capture_notifications に渡すだけで機能します。
def test_cache_miss_instrumentation
events = capture_notifications(/^cache_(.*)\. active_support$/) do
assert_not @cache.fetch(SecureRandom.uuid) { }
end
assert_equal 3, events.length
assert_equal "cache_read.active_support", events[0].name
assert_equal "cache_generate.active_support", events[1].name
assert_equal "cache_write.active_support", events[2].name
assert_equal :fetch, events[0].payload[:super_operation]
assert_not events[0].payload[:hit]
end
変更量はAdditions・Deletionsともに20行で、アサーション内容は一切変わっていません。インスタンス変数 @events がローカル変数 events に変わり、ensure ブロックが丸ごと削除されています。
設計判断
capture_notifications ブロックでキャプチャスコープを明示する方式 が採用されています。
変更前のパターンでは、@cache.write の呼び出しもサブスクライバー登録後に行われていたため、write 自体が発火するイベントも意図せずキャプチャされる余地がありました。変更後は write をブロック外で先に実行し、fetch のみをブロック内に囲んでいます。これにより、テストが検証したい操作とキャプチャ範囲が一致し、テストの意図がコードの構造から直接読み取れるようになっています。
また、ensure ブロックによるクリーンアップの廃止は、Notifications.unsubscribe の呼び忘れや引数ミスによるテスト間干渉リスクを排除します。capture_notifications がサブスクライバーのライフサイクルをブロックスコープで管理するため、テストの独立性がヘルパー側で保証されます。
まとめ
この変更は、テストの振る舞いを変えずにキャッシュストアのInstrumentationテストを整理したリファクタリングです。capture_notifications によるスコープの明示化と自動クリーンアップの採用により、テストコードの意図が構造から読み取れるようになり、サブスクライバー管理の漏れによる副作用リスクも解消されています。