ActionMailerのdeliver_laterでメール送信する場合のユニットテストの書き方

社内ドキュメントに書いたやつを転載。

課題

deliver_laterを使う場合、Railsガイドのメイラーの機能テストの項にあるような、 ActionMailer::Base.deliveries.last での検証はできません。 これは、deliver_laterは非同期でのメール送信となるため、即時のメール送信結果の作成が行われないからです。

これに対しては以下の二つのアプローチがあります。

ここでは便利なのでrspecを利用した例を書いていますが、TestHelperを使えばminitestでも同様のテストは可能です。

対応1: ActiveJobのキューを検証する

基本的にこの対応がいいと思います。 コントローラーやモデルでは、キューにいれるまでが責務で、渡された情報をどのようにメールに組み立てるかはActionMailerのテストで賄うべきだからです。

RSpecではマッチャーが用意されており、 have_enqueued_mail を使うことで簡易に検証できます。

RSpec.describe NotificationsMailer do
  it "matches with enqueued mailer" do
    expect {
      NotificationsMailer.signup.deliver_later
    }.to have_enqueued_mail(NotificationsMailer, :signup)
  end
end

https://relishapp.com/rspec/rspec-rails/v/5-0/docs/matchers/have-enqueued-mail-matcher

もし、Mailerを呼び出す側でエンキューするときの引数を組み立てるロジックがある場合でも、 .with で引数を確認するくらいが適切です。 ActionMailer::Base.deliveries.last の中身の確認はやりすぎ。

rspecでない場合は、ActiveJob::TestHelper を使って、 ActionMailer::MailDeliveryJob のジョブが入っているかなどを確認するのが良いでしょう。

対応2: 局所的に即時実行にして送信結果を作成する

対応1がおすすめですが、どうしてもメール内容を確認したいという場合はこちらも使えます。

perform_enqueued_jobs というAPIが用意されているのでそれを使います。 https://edgeapi.rubyonrails.org/classes/ActiveJob/TestHelper.html#method-i-perform_enqueued_jobs

ActiveJob::TestHelper

RSpec.describe NotificationsMailer do
  it "matches mail result" do
    perform_enqueued_jobs(only: ActionMailer::MailDeliveryJob) do
      expect(NotificationsMailer.signup.deliver_later).to change(ActionMailer::Base.deliveries, :count).by(1)
      mail = ActionMailer::Base.deliveries.last
      expect( mail.subject).to eq 'mail subject'
    end
  end
end

局所的に設定変更するやり方の記事もありますが、これだと全部のJobが動いてしまうので悪手です。有効にするJobを選べる、 perform_enqueued_jobs を使う方が賢い。