【rails】ほとんど見当たらないdelayed_jobのrspecまとめ

ことの始まり

前回の記事でdelayed_jobを導入してみた。
yasagori-programing.hatenablog.jp

導入したのはいいものの、rspecの記事がほとんど見つからない…
これは非常に困ったので、そのやり方をまとめておく。

テストの考え方

まずは考え方を整理しておく。
今まではcontrollerでメールの送信を行っていたので、当然controllerのrspecでは次のような確認をしていた。

class OrdersController < ApplicationController
  def  submit
    ~~~  #ここにはorderを保存する処理とかがいるはず
    OrderMailer.send(order).deliver
  end
end
RSpec.describe OrdersController, type: :controller do
  describe 'メールが送信できること' do
    before do
      allow(OrderMailer).to receive_message_chain(:send, :deliver)
    end

    it 'success' do
      ~~~  # 前処理は省略
      expect(OrderMailer.send).to have_received(:deliver).once
    end

    it 'fail' do
      ~~~  # 前処理は省略
      expect(OrderMailer.send).not_to have_received(:deliver)
    end
  end
end

しかし、delayed_jobでメール送信が非同期化されたのでcontrollerではジョブを登録するメソッドが呼び出されたこと確認をすれば良い。
メールの送信に関する確認はジョブのテストで確認することにする。

controllerのrspecでジョブメソッドの呼び出し確認

delayed_jobを使ったcontrollerのコードはこうなっている。

  def submit
    OrderMailDeliverJob.perform_later(order)
  end

ここからメソッドの呼び出し確認テストに変更する。

RSpec.describe OrdersController, type: :controller do
  describe 'メール送信ジョブが呼ばれている' do
    before do
      allow(OrderMailDeliverJob).to receive(:perform_later)
    end

    it 'success' do
      ~~~  # 前処理は省略
      expect(OrderMailDeliverJob).to have_received(:perform_later)
    end

    it 'fail' do
      ~~~  # 前処理は省略
      expect(OrderMailDeliverJob).not_to have_received(:perform_later)
    end
  end
end

jobのrspec

そして難関。jobのrspecのやり方。
事前にjobのコードがどうなってるかを確認。

class OrderMailDeliverJob < ApplicationJob
  queue_as :default

  def perform(order)
    OrderMailer.send(order).deliver_now
  end
end
class ApplicationJob
  attr_reader :attempt_number

  RETRY_LIMIT = 3

  rescue_from(StandardError) do |e|
    if retry_limit_exceeded?
      Bugsnag.notify(e)
    else  # 上限以下ならリトライ
      retry_job(wait: wait_time)
    end
  end

  def serialize
    super.merge("attempt_number" => (@attempt_number || 0) + 1)
  end

  def deserialize(job_data)
    super
    @attempt_number = job_data["attempt_number"]
  end

  private

  def retry_limit_exceeded?
    @attempt_number >= RETRY_LIMIT
  end

  def wait_time
    attempt_number**2
  end
end

確認したいのはひとまずメール送信の成功と、リトライ含めて失敗したら通知が飛ぶの2パターン。
下準備としてrails_helperに次のコードを追加しておく。

RSpec.configure do |config|
  config.include ActiveJob::TestHelper
end

まずはメール送信成功のパターンから。

require 'rails_helper'

RSpec.describe OrderMailDeliverJob, type: :job do
  after do
    Delayed::Job.delete_all  # test用のテーブルを空にしておく
  end
  context 'メール送信' do
    before do
      allow(OrderMailer).to receive_message_chain(:send, :deliver_now)
    end

    it '成功' do
      expect(OrderMailer.send).to receive(:deliver_now).once
      OrderMailDeliverJob.perform_later(order)  # orderはFactoryBotとかで先に作っておく必要あり
    end
  end
end

いよいよメール送信失敗のパターン。
確認したいのはリトライが設定回数行われていること通知が飛んでいること

  context '規定回数メール送信失敗' do
    let(:retry_count) { ApplicationJob::RETRY_LIMIT }
    before do
      allow(OrderMailer).to receive_message_chain(:send, :deliver_now).and_raise(RuntimeError)
      allow(Bugsnag).to receive(:notify)
      allow_any_instance_of(ApplicationJob).to receive(:wait_time).and_return(0)
    end

    it 'bugsnagに通知' do
      expect(OrderMailer.send).to receive(:deliver_now).exactly(retry_count).times
      expect(Bugsnag).to receive(:notify).once

      OrderMailDeliverJob.perform_later(order)
    end
  end

ジョブの登録・実行を自分で設定したい

rails_helperに追加したActiveJob::TestHelperは賢いのでperform_laterでジョブの実行までやってくれる。
逆にいうとジョブが実行されてしまうので、ジョブが登録されているかの確認ができない。
そこであえてActiveJob::TestHelperを使わない場合は自分でジョブの実行まで行う必要がある。

  context 'メール送信' do
    before do
      allow(OrderMailer).to receive_message_chain(:send, :deliver_now)
    end

    it '成功' do
      expect(OrderMailer.send).to receive(:deliver_now).once
      # jobが登録されていることの確認
      expect { OrderMailDeliverJob.perform_later(order) }.to change(Delayed::Job, :count).by(1)
      # jobを実行
      Delayed::Worker.new.run(Delayed::Job.first)
    end
  end

  context '規定回数メール送信失敗' do
    let(:retry_count) { ApplicationJob::RETRY_LIMIT }
    before do
      allow(OrderMailer).to receive_message_chain(:send, :deliver_now).and_raise(RuntimeError)
      allow(Bugsnag).to receive(:notify)
      allow_any_instance_of(ApplicationJob).to receive(:wait_time).and_return(0)
    end

    it 'bugsnagに通知' do
      expect(OrderMailer.send).to receive(:deliver_now).exactly(retry_count).times
      expect(Bugsnag).to receive(:notify).once

      OrderMailDeliverJob.perform_later(order)
      # jobをリトライ回数実行
      retry_count.times do
        job = Delayed::Job.first
        Delayed::Worker.new.run(job)
      end
    end
  end

まとめ

今回ハマったのはActiveJob::TestHelperを導入しているのに登録ジョブのカウントを取ろうとしたからだった。
導入しないメリットは例えば「2回失敗したけど、3回目で成功した」みたいなパターンのテストを作ることができる。
とはいえ、そこまでやるのはかなり骨が折れるので、実際は最初に書いたやり方で十分だと思われる。