10. Testing Your Mailers

10.1. Keeping the postman in check

Your ActionMailer — like every other part of your Rails application — should be tested to ensure that it is working as expected.

The goal of testing your ActionMailer is to ensure that:

  • emails are being processed (created and sent)

  • the email content is correct (subject, sender, body, etc)

  • the right emails are being sent at the righ times

10.1.1. From all sides

There are two aspects of testing your mailer, the unit tests and the functional tests. Unit tests

In the unit tests, we run the mailer in isolation with tightly controlled inputs and compare the output to a known-value — a fixture — yay! more fixtures!

10.1.2. Functional tests

In the functional tests we don't so much test the minute details produced by the mailer, instead we test that our controllers and models are using the mailer in the right way. We test to prove that the right email was sent at the right time.

10.2. Unit Testing

In order to test that your mailer is working as expected, we can use unit tests to compare the actual results of the mailer with pre-writen examples of what should be produced.

10.2.1. Revenge of the fixtures

For the purposes of unit testing a mailer, fixtures are used to provide an example of how output “should” look. Because these are example emails, and not Active Record data like the other fixtures, they are kept in their own subdirectory from the other fixtures. Don't tease them about it though, they hate that.

When you generated your mailer (you did that right?) the generator created stub fixtures for each of the mailers actions. If you didn't use the generator you'll have to make those files yourself.

10.2.2. The basic test case

Here is an example of what you start with.

require File.dirname(__FILE__) + '/. ./test_helper'

class MyMailerTest < Test::Unit::TestCase
  FIXTURES_PATH = File.dirname(__FILE__) + '/. ./fixtures'

  def setup
    ActionMailer::Base.delivery_method = :test
    ActionMailer::Base.perform_deliveries = true
    ActionMailer::Base.deliveries = []

    @expected = TMail::Mail.new
  end

  def test_signup
    @expected.subject = 'MyMailer#signup'
    @expected.body    = read_fixture('signup')
    @expected.date    = Time.now

    assert_equal @expected.encoded, MyMailer.create_signup(@expected.date).encoded
  end

  private
    def read_fixture(action)
      IO.readlines("#{FIXTURES_PATH}/my_mailer/#{action}")
    end
end

The setup method is mostly concerned with setting up a blank slate for the next test. However it is worth describing what each statement does

ActionMailer::Base.delivery_method = :test

sets the delivery method to test mode so that email will not actually be delivered (useful to avoid spamming your users while testing) but instead it will be appended to an array (ActionMailer::Base.deliveries).

ActionMailer::Base.perform_deliveries = true

Ensures the mail will be sent using the method specified by ActionMailer::Base.delivery_method, and finally

ActionMailer::Base.deliveries = []

sets the array of sent messages to an empty array so we can be sure that anything we find there was sent as part of our current test.

However often in unit tests, mails will not actually be sent, simply constructed, as in the example above, where the precise content of the email is checked against what it should be. Dave Thomas suggests an alternative approach, which is just to check the part of the email that is likely to break, i.e. the dynamic content. The following example assumes we have some kind of user table, and we might want to mail those users new passwords:

require File.dirname(__FILE__) + '/../test_helper'
require 'my_mailer'

class MyMailerTest < Test::Unit::TestCase
  fixtures :users
  FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
  CHARSET = "utf-8"

  include ActionMailer::Quoting

  def setup
    ActionMailer::Base.delivery_method = :test
    ActionMailer::Base.perform_deliveries = true
    ActionMailer::Base.deliveries = []

    @expected = TMail::Mail.new
    @expected.set_content_type "text", "plain", { "charset" => CHARSET }
  end

  def test_reset_password
    user = User.find(:first)
    newpass = 'newpass'
    response = MyMailer.create_reset_password(user,newpass)
    assert_equal 'Your New Password', response.subject
    assert_match /Dear #{user.full_name},/, response.body
    assert_match /New Password: #{newpass}/, response.body
    assert_equal user.email, response.to[0]
  end

  private
    def read_fixture(action)
      IO.readlines("#{FIXTURES_PATH}/community_mailer/#{action}")
    end

    def encode(subject)
      quoted_printable(subject, CHARSET)
    end
end

and here we check the dynamic parts of the mail, specifically that we use the users' correct full name and that we give them the correct password.

10.3. Functional Testing

Functional testing involves more than just checking that the email body, recipients and so forth are correct. In functional mail tests we call the mail deliver methods and check that the appropriate emails have been appended to the delivery list. It is fairly safe to assume that the deliver methods themselves do their job — what we are probably more interested in is whether our own business logic is sending emails when we expect them to. For example the password reset operation we used an example in the previous section will probably be called in response to a user requesting a password reset through some sort of controller.

require File.dirname(__FILE__) + '/../test_helper'
require 'my_controller'

# Raise errors beyond the default web-based presentation
class MyController; def rescue_action(e) raise e end; end

class MyControllerTest < Test::Unit::TestCase

  def setup
    @controller = MyController.new
    @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new
  end

  def test_reset_password
    num_deliveries = ActionMailer::Base.deliveries.size
    post :reset_password, :email => 'bob@test.com'

    assert_equal num_deliveries+1, ActionMailer::Base.deliveries.size
  end

end

10.4. Filtering emails in development

Sometimes you want to be somewhere inbetween the :test and :smtp settings. Say you're working on your development site, and you have a few testers working with you. The site isn't in production yet, but you'd like the testers to be able to receive emails from the site, but no one else. Here's a handy way to handle that situation, add this to your environment.rb or development.rb file

class ActionMailer::Base

  def perform_delivery_fixed_email(mail)
    destinations = mail.destinations
    if destinations.nil?
      destinations = ["mymail@me.com"]
      mail.subject = '[TEST-FAILURE]:'+mail.subject
    else
      mail.subject = '[TEST]:'+mail.subject
    end
    approved = ["testerone@me.com","testertwo@me.com"]
    destinations = destinations.collect{|x| approved.collect{|y| (x==y ? x : nil)}}.flatten.compact
    mail.to = destinations
    if destinations.size > 0
      mail.ready_to_send
      Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain],
                    server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp|
        smtp.sendmail(mail.encoded, mail.from, destinations)
      end
    end

  end

end