90

trying to upgrade to Rails 4.2, using delayed_job_active_record. I've not set the delayed_job backend for test environment as thought that way jobs would execute straight away.

I'm trying to test the new 'deliver_later' method with RSpec, but I'm not sure how.

Old controller code:

ServiceMailer.delay.new_user(@user)

New controller code:

ServiceMailer.new_user(@user).deliver_later

I USED to test it like so:

expect(ServiceMailer).to receive(:new_user).with(@user).and_return(double("mailer", :deliver => true))

Now I get errors using that. (Double "mailer" received unexpected message :deliver_later with (no args))

Just

expect(ServiceMailer).to receive(:new_user)

fails too with 'undefined method `deliver_later' for nil:NilClass'

I've tried some examples that allow you to see if jobs are enqueued using test_helper in ActiveJob but I haven't managed to test that the correct job is queued.

expect(enqueued_jobs.size).to eq(1)

This passes if the test_helper is included, but it doesn't allow me to check it is the correct email that is being sent.

What I want to do is:

  • test that the correct email is queued (or executed straight away in test env)
  • with the correct parameters (@user)

Any ideas?? thanks

BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
bobomoreno
  • 2,848
  • 5
  • 23
  • 42
  • 1
    See also: [How to check what is queued in ActiveJob using Rspec](https://stackoverflow.com/questions/26274954/how-to-check-what-is-queued-in-activejob-using-rspec) – Jared Beck Oct 02 '15 at 19:34

14 Answers14

89

If I understand you correctly, you could do:

message_delivery = instance_double(ActionMailer::MessageDelivery)
expect(ServiceMailer).to receive(:new_user).with(@user).and_return(message_delivery)
allow(message_delivery).to receive(:deliver_later)

The key thing is that you need to somehow provide a double for deliver_later.

fqxp
  • 7,680
  • 3
  • 24
  • 41
Peter Alfvin
  • 28,599
  • 8
  • 68
  • 106
  • 1
    Shouldn't this be `allow(message_delivery).to …`? After all, you already test the outcome by expecting `new_user`. – morgler Dec 07 '16 at 14:16
  • 1
    @morgler Agreed. I updated the answer. Thanks for noticing/commenting. – Peter Alfvin Dec 21 '16 at 18:22
  • 1
    This may be a little off topic @morgler but I'm curious to know what you or others think in general if say for some reason(like by mistake), the `deliver_later` method is removed from the controller, by using `allow` we will not be able to catch that right? I mean the test will still pass. Do you still think using an `allow` will be a better idea than using `expect`? I did see that `expect` flags if the `deliver_later` was removed by mistake & that's basically why I wanted to discuss this in general. It would be great if you could elaborate more on why `allow` is better with the above context. – boddhisattva Sep 19 '17 at 00:12
  • @boddhisattva a valid point. However, this spec is supposed to test whether the `ServiceMailer`'s `new_user` method is called. You're free to create another test which tests for the `deliver_later` method being called, once the mail was created. – morgler Sep 19 '17 at 06:04
  • @morgler Thanks for your response to my question. I now understand that you've mainly suggested the usage of `allow` based on the context of testing `ServiceMailer's new_user` method. In case I'd have to test `deliver_later`, I was thinking I'd just add another assertion to the existing test(that checks for `ServiceMailer's new_user` method) to check something like `expect(mailer_object).to receive(:deliver_later)` instead of testing this as another test altogether. It would be interesting to know why you'd prefer a separate test for this in case we'd have to ever test `deliver_later`. – boddhisattva Sep 24 '17 at 15:38
  • @boddhisattva I prefer granular Tests that immediately tell me what went wrong. The more assertions I pack into a single test, the more I need to look at the test code and understands, which assertion failed and why. Would my test read „ensure deliver_later is present“, I‘d know exactly what went wrong. Besides that, an allow would give you the chance to allow deliver_now AND deliver_later, so that you could change this aspect of your mailing code without affecting this spec. But to be clear: expecting instead of allowing is totally fine as well. – morgler Sep 26 '17 at 11:56
  • Thanks for answering my question @morgler. I now better understand where you are coming from and I do agree in general with granular tests you better understand what's going on through the test description without actually having to look into the implementation details to understand what going on under the hood. Yep, also using `allow` has it's own pros and cons and it depends on what you're primarily looking for to cover as part of your test and what kind of flexibility you want when looking back in terms of your code. Thanks for this conversation and for answering my questions :) – boddhisattva Oct 12 '17 at 14:32
61

Using ActiveJob and rspec-rails 3.4+, you could use have_enqueued_job like this:

expect { 
  YourMailer.your_method.deliver_later 
  # or any other method that eventually would trigger mail enqueuing
}.to( 
  have_enqueued_job.on_queue('mailers').with(
    # `with` isn't mandatory, but it will help if you want to make sure is
    # the correct enqueued mail.
    'YourMailer', 'your_method', 'deliver_now', any_param_you_want_to_check
  )
)

also double check in config/environments/test.rb you have:

config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test

Another option would be to run inline jobs:

config.active_job.queue_adapter = :inline

But keep in mind this would affect the overall performance of your test suite, as all your jobs will run as soon as they're enqueued.

Alter Lagos
  • 12,090
  • 1
  • 70
  • 92
  • 7
    probably shall check `have_enqueued_mail` for now, https://relishapp.com/rspec-staging/rspec-rails/docs/matchers/have-enqueued-mail-matcher – new2cpp Apr 10 '21 at 13:23
38

If you find this question but are using ActiveJob rather than simply DelayedJob on its own, and are using Rails 5, I recommend configuring ActionMailer in config/environments/test.rb:

config.active_job.queue_adapter = :inline

(this was the default behavior prior to Rails 5)

Gabe Kopley
  • 16,281
  • 5
  • 47
  • 60
  • Wouldn't it perform all asynchronous tasks while running specs? – Aleksey May 03 '17 at 14:28
  • Yes it would, that's a good point. This is handy in simple and light use cases of ActiveJob, where you can configure all async tasks to run inline, and it makes testing simple. – Gabe Kopley Jun 16 '17 at 07:06
  • 1
    Probably just saved me an hour of debugging. Thanks! – Matt Jul 17 '17 at 16:55
  • This used to work beautifully, but seems to have stopped in a recent bundle update :( - any ideas? – Hackeron Jul 21 '18 at 16:41
37

I will add my answer because none of the others was good enough for me:

1) There is no need to mock the Mailer: Rails basically does that already for you.

2) There is no need to really trigger the creation of the email: this will consume time and slow down your test!

That's why in environments/test.rb you should have the following options set:

config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test

Again: don't deliver your emails using deliver_now but always use deliver_later. That prevents your users from waiting for the effective delivering of the email. If you don't have sidekiq, sucker_punch, or any other in production, simply use config.active_job.queue_adapter = :async. And either async or inline for development environment.

Given the following configuration for the testing environment, you emails will always be enqueued and never executed for delivery: this prevents your from mocking them and you can check that they are enqueued correctly.

In you tests, always split the test in two: 1) One unit test to check that the email is enqueued correctly and with the correct parameters 2) One unit test for the mail to check that the subject, sender, receiver and content are correct.

Given the following scenario:

class User
  after_update :send_email

  def send_email
    ReportMailer.update_mail(id).deliver_later
  end
end

Write a test to check the email is enqueued correctly:

include ActiveJob::TestHelper
expect { user.update(name: 'Hello') }.to have_enqueued_job(ActionMailer::DeliveryJob).with('ReportMailer', 'update_mail', 'deliver_now', user.id)

and write a separate test for your email

Rspec.describe ReportMailer do
    describe '#update_email' do
      subject(:mailer) { described_class.update_email(user.id) }
      it { expect(mailer.subject).to eq 'whatever' }
      ...
    end
end
  • You have tested exactly that your email has been enqueued and not a generic job.
  • Your test is fast
  • You needed no mocking

When you write a system test, feel free to decide if you want to really deliver emails there, since speed doesn't matter that much anymore. I personally like to configure the following:

RSpec.configure do |config|
  config.around(:each, :mailer) do |example|
    perform_enqueued_jobs do
      example.run
    end
  end
end

and assign the :mailer attribute to the tests were I want to actually send emails.

For more about how to correctly configure your email in Rails read this article: https://medium.com/@coorasse/the-correct-emails-configuration-in-rails-c1d8418c0bfd

coorasse
  • 5,278
  • 1
  • 34
  • 45
  • 3
    Just had to change class to `ActionMailer::MailDeliveryJob` instead of `ActionMailer::DeliveryJob` – haffla Mar 28 '19 at 21:39
  • This is a great answer! – Holger Frohloff Nov 19 '20 at 14:12
  • Thx! On Rails 6 I had only to change ```have_enqueued_job(ActionMailer::DeliveryJob)``` to ```on_queue('mailers')```, so it became ```expect { user.update(name: 'Hello') }.to have_enqueued_job.on_queue('mailers').with('ReportMailer', 'update_mail', 'deliver_now', user.id)``` – Pedro Schmitt Aug 22 '21 at 19:20
  • 2
    As a variation of this approach, you can check with `have_enqueued_mail` matcher, see https://relishapp.com/rspec/rspec-rails/v/5-0/docs/matchers/have-enqueued-mail-matcher – Benjamin Sep 02 '21 at 15:37
12

Add this:

# spec/support/message_delivery.rb
class ActionMailer::MessageDelivery
  def deliver_later
    deliver_now
  end
end

Reference: http://mrlab.sk/testing-email-delivery-with-deliver-later.html

Minimul
  • 4,060
  • 2
  • 21
  • 18
  • 5
    This worked for me but I was using `deliver_later(wait: 2.minutes)`. So I did `deliver_later(options={})` – rigelstpierre Oct 14 '15 at 10:03
  • 8
    Apps can send sync and async emails, this is a hack that would make it impossible to tell the difference in tests. – Jeriko Oct 30 '15 at 19:51
  • 2
    I agree that the hack is a bad idea. Aliasing _later to _now will only end in pain. – John Paul Ashenfelter May 18 '16 at 17:11
  • 2
    That link is dead, but I found it on the way back machine; http://web.archive.org/web/20150710184659/http://www.mrlab.sk/testing-email-delivery-with-deliver-later.html – OzBarry Mar 23 '17 at 13:58
  • I get `NameError: uninitialized constant ActionMailer` – Albert Català Dec 24 '17 at 18:32
  • Remember to include the helper in relevant specs or spec_helper/rails_helper unless you're set up to autoload. This worked for me in feb 2020 with rails 6 rspec latest. I am not testing any sync vs async emails, just wanted a quick fix without much hassle. – Nick M Feb 27 '20 at 09:53
11

A nicer solution (than monkeypatching deliver_later) is:

require 'spec_helper'
include ActiveJob::TestHelper

describe YourObject do
  around { |example| perform_enqueued_jobs(&example) }

  it "sends an email" do
    expect { something_that.sends_an_email }.to change(ActionMailer::Base.deliveries, :length)
  end
end

The around { |example| perform_enqueued_jobs(&example) } ensures that background tasks are run before checking the test values.

Qqwy
  • 5,214
  • 5
  • 42
  • 83
  • This approach is definitely more intuitive to understand, but can slow down your tests substantially if the subject action enqueues any time-consuming jobs. – niborg Sep 26 '16 at 21:39
  • This also does not test which mailer / action is being picked. If your code involves conditionally picking a different mail it won't help – Cyril Duchon-Doris Apr 29 '17 at 11:48
5

I came with the same doubt and resolved in a less verbose (single line) way inspired by this answer

expect(ServiceMailer).to receive_message_chain(:new_user, :deliver_later).with(@user).with(no_args)

Note that the last with(no_args) is essential.

But, if you don't bother if deliver_later is being called, just do:

expect(ServiceMailer).to expect(:new_user).with(@user).and_call_original

Community
  • 1
  • 1
Luccas
  • 4,078
  • 6
  • 42
  • 72
4

A simple way is:

expect(ServiceMailer).to(
  receive(:new_user).with(@user).and_call_original
)
# subject
Nuno Silva
  • 728
  • 11
  • 27
3

For recent Googlers:

allow(YourMailer).to receive(:mailer_method).and_call_original

expect(YourMailer).to have_received(:mailer_method)
crowns4days
  • 963
  • 1
  • 6
  • 6
2

This answer is for Rails Test, not for rspec...

If you are using delivery_later like this:

# app/controllers/users_controller.rb 

class UsersController < ApplicationController 
  … 
  def create 
    … 
    # Yes, Ruby 2.0+ keyword arguments are preferred 
    UserMailer.welcome_email(user: @user).deliver_later 
  end 
end 

You can check in your test if the email has been added to the queue:

# test/controllers/users_controller_test.rb 

require 'test_helper' 

class UsersControllerTest < ActionController::TestCase 
  … 
  test 'email is enqueued to be delivered later' do 
    assert_enqueued_jobs 1 do 
      post :create, {…} 
    end 
  end 
end 

If you do this though, you’ll surprised by the failing test that tells you assert_enqueued_jobs is not defined for us to use.

This is because our test inherits from ActionController::TestCase which, at the time of writing, does not include ActiveJob::TestHelper.

But we can quickly fix this:

# test/test_helper.rb 

class ActionController::TestCase 
  include ActiveJob::TestHelper 
  … 
end 

Reference: https://www.engineyard.com/blog/testing-async-emails-rails-42

maurymmarques
  • 329
  • 1
  • 3
  • 17
1

I think one of the better ways to test this is to check the status of job alongside the basic response json checks like:

expect(ActionMailer::MailDeliveryJob).to have_been_enqueued.on_queue('mailers').with('mailer_name', 'mailer_method', 'delivery_now', { :params => {}, :args=>[] } )
sabhari karthik
  • 1,361
  • 6
  • 17
0

I have come here looking for an answer for a complete testing, so, not just asking if there is one mail waiting to be sent, in addition, for its recipient, subject...etc

I have a solution, than comes from here, but with a little change:

As it says, the curial part is

mail = perform_enqueued_jobs { ActionMailer::DeliveryJob.perform_now(*enqueued_jobs.first[:args]) }

The problem is that the parameters than mailer receives, in this case, is different from the parameters than receives in production, in production, if the first parameter is a Model, now in testing will receive a hash, so will crash

enqueued_jobs.first[:args]
["UserMailer", "welcome_email", "deliver_now", {"_aj_globalid"=>"gid://forjartistica/User/1"}]

So, if we call the mailer as UserMailer.welcome_email(@user).deliver_later the mailer receives in production a User, but in testing will receive {"_aj_globalid"=>"gid://forjartistica/User/1"}

All comments will be appreciate, The less painful solution I have found is changing the way that I call the mailers, passing, the model's id and not the model:

UserMailer.welcome_email(@user.id).deliver_later

Albert Català
  • 2,026
  • 2
  • 29
  • 35
0

This answer is a little bit different, but may help in cases like a new change in the rails API, or a change in the way you want to deliver (like use deliver_now instead of deliver_later).

What I do most of the time is to pass a mailer as a dependency to the method that I am testing, but I don't pass an mailer from rails, I instead pass an object that will do the the things in the "way that I want"...

For example if I want to check that I am sending the right mail after the registration of a user... I could do...

class DummyMailer
  def self.send_welcome_message(user)
  end
end

it "sends a welcome email" do
  allow(store).to receive(:create).and_return(user)
  expect(mailer).to receive(:send_welcome_message).with(user)
  register_user(params, store, mailer)
end

And then in the controller where I will be calling that method, I would write the "real" implementation of that mailer...

class RegistrationsController < ApplicationController
  def create
    Registrations.register_user(params[:user], User, Mailer)
    # ...
  end

  class Mailer
    def self.send_welcome_message(user)
      ServiceMailer.new_user(user).deliver_later
    end
  end
end

In this way I feel that I am testing that I am sending the right message, to the right object, with the right data (arguments). And I am just in need of creating a very simple object that has no logic, just the responsibility of knowing how ActionMailer wants to be called.

I prefer to do this because I prefer to have more control over the dependencies I have. This is form me an example of the "Dependency inversion principle".

I am not sure if it is your taste, but is another way to solve the problem =).

Benito Serna
  • 1,301
  • 2
  • 9
  • 11
0

In rspec-rails 6 you can do

it 'sends an email' do
  expect {
    do_stuff
  }.to have_enqueued_mail(ServiceMailer, :new_user)
    .with(an_instance_of(User))
end

Here's the doc

Leticia Esperon
  • 2,499
  • 1
  • 18
  • 40