Test ActionMailer `deliver_later` in RSpec Controller Tests
Tags: rails, ruby, testing, webdev
Published: 2021-06-29 00:16:17

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`](https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/InlineAdapter.html), then a `deliver_later` will happen synchronously. So, the email will immediately end up in the `deliveries` box.

```ruby
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`](https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/TestAdapter.html) 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.

```ruby
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:

```ruby
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
```

[docs](https://relishapp.com/rspec-staging/rspec-rails/docs/matchers/have-enqueued-mail-matcher)

## 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.

```ruby
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.

```ruby
# 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.

```ruby
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](https://crafty-builder-6996.ck.page/e169c61186) or following me on [twitter](https://twitter.com/jbrancha)._

---

References:
- [Test If deliver_later Is Called For A Mailer](https://github.com/jbranchaud/til/blob/master/rails/test-if-deliver-later-is-called-for-a-mailer.md)
- [Related StackOverflow Question](https://stackoverflow.com/questions/27647749/how-to-test-actionmailer-deliver-later-with-rspec)
- [`ActionMailer::TestHelper`](https://api.rubyonrails.org/v6.0.3.6/classes/ActionMailer/TestHelper.html)

---

<em>Cover photo by <a href="https://unsplash.com/@timothyeberly?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Timothy Eberly</a> on <a href="https://unsplash.com/s/photos/mail?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></em>