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

ジョブの実行をテストしたいとき、キューに入ったことをテストしたいだけのときと、その実行結果まで含めてテストしたいとき（つまり同期実行したいとき）がある。
前者であれば`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を同期実行してくれる。

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

```rb: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 単位で同期実行するかどうかを切り替えられたら便利。
（もちろんできることなら範囲を絞れたほうが良いし、それができないのであれば同期実行のテストは別に分けるほうが良い…）

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

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

```rb:spec/features/hoge_spec.rb
  describe '同期実行', :perform_enqueued_jobs do
    # この中の exmaple では perform_later が同期実行される 
  end
```

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

```rb:spec/rails_helper.rb
RSpec.configure do |config|
  config.when_first_matching_example_defined(type: :feature) do
    require 'support/perform_enqueued_jobs'
  end
end
```

```rb: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 を使えなかったためにこうなってしまっている。
理想的には

```rb
  config.around(:each, :perform_enqueued_jobs) do |example|
    perform_enqueued_jobs do
      example.run
    end
  end
```

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