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.