How to Mock ActionMailer

Using RSpec custom matchers to make clean ActionMailer mocks

The Rails ActionMailer is a pretty useful tool.

Unfortunately, it's extremely awkward to mock for tests.

Since these emails are critical to our business I spent some time looking at how to write clean test to make sure my mailers are called when they should be.

The Business Case

So we're working on a Point of Sale for the the Frosty Treat in Kengsington PEI. The CEO is all about data and dopamine rushes, so they want to get an email every time a banana gets split...for whatever reason.

Here's what our test should roughly look like:

spec/banana_spec.rb

it 'sends an :banana_split email after being #split' do
  banana = Banana.new

  # expect BananaMailer to send a :banana_split email later

  banana.split
end

Mocking out the BananaMailer

The ActionMailer API is nice to work with...but involves a lot of weird objects. After a bit of fiddling around and searching through StackOverflow we find the magic spell we need:

spec/banana_spec.rb

it 'sends an :banana_split email after being #split' do
  mailer = double(:mailer)
  mail = double(:mail)

  expect(EmployeeOnboardingMailer).to receive(:with).and_return(mailer)
  expect(mailer).to receive(expected_method).and_return(mail)
  expect(mail).to receive(:deliver_later)

  banana = FactoryBot.create(:banana)

  banana.split
end

Great! It works.

It's a mess though, and the thought of writing that every time I need to test a mailer is enough to keep me from ever writing a mailer test again.

Luckily there's a way around this.

Custom RSpec matchers to the rescue

RSpec makes it easy to create custom matchers for your tests.

Let's build a super simple example:

spec/action_mailer_helper.rb

RSpec::Matchers.define :deliver_later do |expected_method|
  match do |mailer_class|
    mailer = double(:mailer)
    mail = double(:mail)

    expect(mailer_class).to receive(:with).and_return(mailer)
    expect(mailer).to receive(expected_method).and_return(mail)
    expect(mail).to receive(:deliver_later)
  end
end

And don't forget to add it to your rails_helper to make it available.

spec/rails_helper.rb

require 'rails_helper'
+ require 'action_mailer_helper'

Simplifying our test

With our new helper added we can really clean things up in our tests:

spec/banana_spec.rb

it 'sends an :banana_split email after being #split' do
  expect(BananaMailer).to deliver_later(:banana_split)

  banana = Banana.new

  banana.split
end

Voila!

A clear spec to make sure the Frosty Treat CEO will be happy for a long time.