Josh Branchaud
Posted on June 29, 2021
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:
- Test If deliver_later Is Called For A Mailer
- Related StackOverflow Question
ActionMailer::TestHelper
Cover photo by Timothy Eberly on Unsplash
Posted on June 29, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.