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
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!
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.
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.
The basic test case
Here is an example of what you start with.
require File.dirname(__FILE__) + '/. ./test_helper'
require 'my_mailer'
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