ActiveJob キューをテスト間でクリアしてジョブリークを防止
リード文: have_been_enqueued と have_been_performed がテスト間で残存ジョブを誤検知する問題を解消するため、RSpec のジョブ例グループにキューのクリア処理を導入しました。これにより、Rails の実装と同等のテスト分離が保証されます。
背景
このセクションでは、ジョブリークが発生した経緯とその影響を説明します。
have_been_enqueued と have_been_performed はキューアダプターの enqueued_jobs / performed_jobs 配列を直接参照しますが、テスト環境では同一の TestAdapter インスタンスが再利用されるため、ジョブが例間で蓄積されてしまいます。その結果、後続の例が以前の例で残ったジョブを見て失敗するという不具合が報告されました(#2900)。
RSpec 自身のマッチャーは ActiveJob::Base.queue_adapter = :test を before フックで再設定しているため、内部的には毎例新しい TestAdapter が生成され、リークが隠蔽されていました。この実装差が実アプリでの挙動と乖離していることが問題視されました。
以上の背景から、テストフレームワーク側でキュー状態を例毎にリセットする必要があると判断されました。
技術的な変更
このセクションでは、具体的なコード変更点とその動作を示します。
RailsExampleGroup に ActiveJob::TestHelper をインクルード することで、テスト実行前にキューをリセットするヘルパーメソッドが利用可能になります。変更は lib/rspec/rails/example/rails_example_group.rb に以下のように追加されました。
module RailsExampleGroup
module ClassMethods
def included(base)
super
include RSpec::Rails::TaggedLoggingAdapter
include ActiveSupport::CurrentAttributes::TestHelper
include ActiveSupport::ExecutionContext::TestHelper
+
+ if RSpec::Rails::FeatureCheck.has_active_job?
+ include ActiveJob::TestHelper
+ end
end
end
end
ActiveJob::TestHelper が提供する clear_enqueued_jobs と clear_performed_jobs は before フックで自動的に呼び出され、テスト例開始時にキューが空になることを保証します。
テストケースの追加 により、修正が正しく機能することが確認されています。spec/rspec/rails/example/rails_example_group_spec.rb に追加された例では、2 つの例を順に実行し、2 番目の例でキューが空であることを検証しています。
it 'will not leak enqueued ActiveJob jobs between examples', skip: !RSpec::Rails::FeatureCheck.has_active_job? do
original_adapter = ActiveJob::Base.queue_adapter
original_logger = ActiveJob::Base.logger
ActiveJob::Base.queue_adapter = :test
ActiveJob::Base.logger = Logger.new(nil)
leaky_job = Class.new(ActiveJob::Base) do
def perform; end
def self.name; 'LeakyJob' end
end
group = RSpec::Core::ExampleGroup.describe('A group', order: :defined) do
include RSpec::Rails::RailsExampleGroup
it 'enqueues a job' do
leaky_job.perform_later
expect(enqueued_jobs.size).to eq(1)
end
it 'does not see the job enqueued in the prior example' do
expect(enqueued_jobs).to be_empty
end
end
expect(group.run(failure_reporter) ? true : failure_reporter.exceptions).to be true
ensure
ActiveJob::Base.queue_adapter = original_adapter
ActiveJob::Base.logger = original_logger
end
これにより、enqueued_jobs と performed_jobs が例毎にリセットされ、マッチャーが正しい状態を評価できるようになります。
設計判断
このセクションでは、採用された実装アプローチの設計意図とトレードオフを整理します。
RailsExampleGroup へのインクルード を選択した理由は、ジョブ例だけでなく全ての Rails 例で同様のクリア動作が必要になる可能性を考慮し、共通基盤に機能を集約することで重複実装を防ぐためです。JobExampleGroup に限定すると、将来的に別のヘルパーでもキューリセットが必要になる場合に修正が散在するリスクがあります。
before フックの採用 は、テスト実行後にキューを残すことで around フック等からの検査を可能にしつつ、例開始時点で常にクリーンな状態を保証するというバランスを取っています。after フックでも実装は可能ですが、ユーザーが after フック内でキューを参照したいケースを考慮し、before が最も汎用的と判断されました。
respond_to? ガード を残したのは、ユーザーがテストアダプタ以外のキューアダプタに切り替えている場合でもエラーにならないように安全性を確保するためです。この点は既存の ActiveJob::TestHelper と同様の実装パターンを踏襲しています。
まとめ
本 PR は、RSpec が Rails の ActiveJob::TestHelper と同等のキューリセット機構を提供することで、テスト間のジョブリークを防止しました。RailsExampleGroup に ActiveJob::TestHelper をインクルードし、before フックでキューをクリアする設計は、後方互換性と拡張性を両立させた選択です。結果として、have_been_enqueued 系マッチャーが正確に動作し、テスト信頼性が向上します。