Posted at

RSpec でキューイングした ActiveJob を同期実行する

More than 1 year has passed since last update.

ジョブの実行をテストしたいとき、キューに入ったことをテストしたいだけのときと、その実行結果まで含めてテストしたいとき(つまり同期実行したいとき)がある。

前者であればRails.application.config.active_job.queue_adapter = :testで足りるが、後者を実現するためにはどうすればよいだろう。

方法はいくつかあって、まずはRails.application.config.active_job.queue_adapter = :inlineとする方法。

これは簡単に同期実行は実現できるが、こんどはキューに入ったことが確認できなくなってしまうし、他の非同期のままにしておきたいところまで影響がでてしまう。

またSidekiqを使っているのであればSidekiq::Testing.inline!Sidekiq::Testing.fake! で制御することはできるが、せっかくActiveJobを使っているのにキューアダプタに依存したモジュールを使うことになってしまう。

(もしSidekiqを直接使うのであれば気にせず使ってよさそう)

もうひとつ、 ActiveJob::TestHelper を使う方法がある。今回はその使い方をまとめてみた。


ActiveJob::TestHelper#perform_enqueued_jobs

ActiveJob::TestHelperをincludeして使えるようになる#perform_enqueued_jobsは、ブロック内のActiveJobを同期実行してくれる。


spec/rails_helper.rb

RSpec.configure do |config|

# 各 example で読み込まれ、 activejob.queue_adapter を ActiveJob::QueueAdapter::TestAdapter にセットする
# perform_enqueued_jobs などのテストヘルパーが使えるようになる
config.include ActiveJob::TestHelper
end


spec/mailers/hoge\_mailer\_spec.rb

  describe '同期実行' do

before do
perform_enqueued_jobs do
HogeMailer.welcome.deliver_later
end
end

it 'すぐに送信すること' do
expect(HogeMailer.deliveries.size).to eq 1
end
end


この方法なら同期実行がブロック内だけに限られるので、他の非同期に実行したい(つまりテストでは実行したくない)ところに影響を及ぼすことはないし、キューに入ったことだけをテストすることもできる。万事解決だ。

しかしいつもこんなふうにブロックで囲める状態なら良いのだが、feature spec などでは狙ってエンキューのタイミングでブロックを囲めないときもある。

そんなときはメタタグに登録して example 単位で同期実行するかどうかを切り替えられたら便利。

(もちろんできることなら範囲を絞れたほうが良いし、それができないのであれば同期実行のテストは別に分けるほうが良い…)


メタタグで同期実行の切り替えを制御する

使う際のイメージはこんな感じ。


spec/features/hoge_spec.rb

  describe '同期実行', :perform_enqueued_jobs do

# この中の exmaple では perform_later が同期実行される
end

こうできるように、メタタグを登録していく。


spec/rails_helper.rb

RSpec.configure do |config|

config.when_first_matching_example_defined(type: :feature) do
require 'support/perform_enqueued_jobs'
end
end


spec/support/perform_enqueued_jobs.rb

RSpec.configure do |config|

# NOTE: ActiveJob::TestHelper#perform_enqueued_jobs と同等のことを before/after で実施
# エンキューのタイミングを狙って perform_enqueued_jobs を仕込むのが難しい場合にこのメタタグを利用する。
#
# before/after で実施しているのは、 around hook で perform_enqueued_jobs { example.run } のようにする方法がなかったため。
# rspec-rails が minitest 用テストヘルパーをつなぎこんでいるところ (MinitestLifecycleAdapter) ですでに
# around hook を使っており、include順序に依存して使用可否が変化するがそれを制御するすべがなかった。
#
# NOTE: 本体の TestHelper の内容変更に注意すること
# https://github.com/rails/rails/blob/7fbfa9864d24df4652286f6d4ef5a236cca94f95/activejob/lib/active_job/test_helper.rb#L291

config.before(:each, :perform_enqueued_jobs) do |example|
@old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs
@old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs

queue_adapter.perform_enqueued_jobs = true
queue_adapter.perform_enqueued_at_jobs = true
end

config.after(:each, :perform_enqueued_jobs) do |example|
queue_adapter.perform_enqueued_jobs = @old_perform_enqueued_jobs
queue_adapter.perform_enqueued_at_jobs = @old_perform_enqueued_at_jobs
end
end


遠回りなことをしているが、要は around hook を使えなかったためにこうなってしまっている。

理想的には

  config.around(:each, :perform_enqueued_jobs) do |example|

perform_enqueued_jobs do
example.run
end
end

とできたら良かったのだが、コメントにある通り難しそうだった。