Test ActionMailer `deliver_later` in RSpec Controller Tests

A lot can happen when a Rails controller action gets called. This includes transactional emails getting queued up for delivery. To ensure our controller's behavior stays consistent as our app evolves we can write RSpec tests.

Among other things these tests can ensure that transactional emails get queued for delivery at the appropriate times.

This post documents a couple different methods I've used for those tests.

ActionMailer::Base.deliveries

If you have your queue_adapter set to :inline, then a deliver_later will happen synchronously. So, the email will immediately end up in the deliveries box.

describe '#welcome' do
  it 'sends the welcome email to the user' do
    valid_params = { user_id: user.id }

    expect {
      post :invite, params: valid_params
    }.to change { ActionMailer::Base.deliveries.count }.by(1)
  end
end

At this point you could even write an additional test to look at properties of the email that was sent, like who it was sent to and what the subject line said.

have_enqueued_job

The behavior is a bit different if your queue_adapter is set to something like :test or async. In this case, the email is going to be queued in the app's job queue. Since it is not immediately being sent, the expectation will have to be about the job queue instead.

describe '#welcome' do
  it 'sends the welcome email to the user' do
    valid_params = { user_id: user.id }

    expect {
      post :invite, params: valid_params
    }.to have_enqueued_job(ActionMailer::DeliveryJob)
  end
end

We can even dig into more specifics about what mailer class and method were invoked, like this:

describe '#welcome' do
  it 'sends the welcome email to the user' do
    valid_params = { user_id: user.id }

    expect {
      post :invite, params: valid_params
    }.to have_enqueued_job(ActionMailer::DeliveryJob)
      .with('UserMailer', 'welcome', 'deliver_now', Integer)
  end
end

Receive Block and Mail Double

This approach mocks the mailer so that we can test that deliver_later gets called. We take things a step further with the receive method by using its &block argument to make assertions about the values passed to the mailer method.

describe '#welcome' do
  it 'sends the welcome email to the user' do
    mail_double = double
    allow(mail_double).to receive(:deliver_later)

    expect(UserMailer).to receive(:welcome) do |user_id|
      expect(user_id).to match(user.id)
    end.and_return(mail_double)

    valid_params = { user_id: user.id }

    post :invite, params: valid_params
  end
end

ActionMailer RSpec Matcher

The previous approach requires a bit of boilerplate setup. If There is a way to go the (instance) double route, without duplicating this setup over and over. That can be achieved with a custom RSpec matcher. I've used some version of the following on many Rails projects.

# spec/support/mailer_matcher.rb
require "rspec/expectations"

RSpec::Matchers.define :send_email do |mailer_action|
  match do |mailer_class|
    message_delivery = instance_double(ActionMailer::MessageDelivery)
    expect(mailer_class).to receive(mailer_action).and_return(message_delivery)
    allow(message_delivery).to receive(:deliver_later)
  end
end

Assuming the spec helper requires support files, this custom matcher will be available in your specs. Here is how to use it.

describe '#welcome' do
  it 'sends the welcome email to the user' do
    expect(UserMailer).to send_email(:welcome)

    valid_params = { user_id: user.id }

    post :invite, params: valid_params
  end
end

These are the approaches I know about and use. If I'm missing an approach to testing ActionMailer, drop a note. I'd love to see how you're doing it.

If you enjoy my writing, consider joining my newsletter or following me on twitter.

References:

Cover photo by Timothy Eberly on Unsplash

38