Nolan Phillips
Nolan Phillips's Blog

Follow

Nolan Phillips's Blog

Follow

How to Mock ActionMailer

Using RSpec custom matchers to make clean ActionMailer mocks

Nolan Phillips's photo
Nolan Phillips
·May 13, 2022·

2 min read

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.

 
Share this