Rails Email Unit Testing

ActionMailer classes, for sending out emails, typically reside with models, inside app/models folder. Yet, they have traits that are similar to controllers. For example, each actionmailer class has a view folder inside app/views where email templates are defined. And testing support for email is provided as functional tests with assert_select macros such as assert_select_email.
In my current project, most of our email hooks are defined on model classes as after_filter. This is in line with recommendation to keep controllers thin and models fat. However, testing email body’s html format is a pain since assert_select_email support is only available in ActionController::TestCase hierarchy, and not in ActionMailer::TestCase hierarchy.
Consider following ActionMailer test:
class MortgageBorrowerEmailerTest < ActionMailer::TestCase should "invite borrowers" do realtor, borrower = User.make, User.make response = MortgageBorrowerEmailer.deliver_invite_borrowers(realtor, borrower) # test from/to/subject works now :-) assert_same_elements([borrower.email], response.to) assert_equal([realtor.email], response.from) assert_equal("#{realtor.full_name} has invited you to manage your mortgage application in Lendable", response.subject) # testing body is still primitive :-( assert response.body.include?(realtor.full_name) assert response.body.include?("/account_preferences/#{borrower.perishable_token}/edit") end end
ActionMailer::TestCase has decent support for intercepting mail delivery and hold onto mails for introspection. This allows us to assert smaller pieces like from, to, subject etc. However, what it doesn’t support is more fine grained assertion on email’s html body. it would be nice to be able to apply assert_select assertion on email’s response body for critical pieces of information that needs to be present in an email.
So, I dug into ActionController::TestCase’s assert_select_email to see how I can duplicate it for my unit test. Here is what the original method looks like:
# [rails]/actionpack/lib/action_controller/assertions/selector_assertions.rb module ActionController::Assertions::SelectorAssertions def assert_select_email(&block) deliveries = ActionMailer::Base.deliveries assert !deliveries.empty?, "No e-mail in delivery list" for delivery in deliveries for part in delivery.parts if part["Content-Type"].to_s =~ /^text\/html\W/ root = HTML::Document.new(part.body).root assert_select root, ":root", &block end end end end end
As this implementation depicts, it is interesting to see how easy it is to take any html code snipped and create an HTML::Document out of it so that assert_select can be applied to it. Anyway, the default implementation for assert_select_email assumed multipart email with one or more parts in it. Since my email are simplistic text/html rendered as email body, I hacked to get what I needed. As follows:
class MortgageBorrowerEmailerTest < ActionMailer::TestCase include ActionController::Assertions::SelectorAssertions def assert_select_with_string(html_text, &block) root = HTML::Document.new(html_text).root assert_select root, ":root", &block end end
With this in place, I could now take my email response’s body and assert-select on it:
should "share mortgage with existing client" do ... assert_select_with_string(response.body) do assert_select "h1", :text => "Greetings from Lendable" assert_select "tr#sender_identity td strong", :text => @realtor.full_name assert_select "tr#rate_search_criteria" do assert_select ".purchase_price" assert_select ".loan_amount" end assert_select "tr#rate_info td a[href=?]", "http://#{DEFAULT_HOST}/rate_selections/#{@rate_selection.id}", :text => "#{@rate.program.type_and_term} Mortgage at #{number_with_precision(@rate.rate, :precision => 3)}% from #{@rate.program.lender.name}" end end
As we can imagine, this assert_select_with_string() can be useful in testing helper methods that return html snippets. I am not sure what the downside is for testing email body format as unit test vs. funcational test but I am happy that I can test it in isolation as unit test.
Do you know of any?
Related posts:
- Login as multiple users simultaneously for testing Ever wondered how to login as two different users...
- Ruby Authlogic: lazy initialization based on defined?() call What is the difference between following two versions of code?...
- Environment specific routing in rails For better application support, it is usually nice to build...
- Internet, Email, Blogs, Forums … did we need twitter? Yes, we did. And here’s why. I was at...
- Ruby ActiveRecord: How to implement has_few using has_one and belongs_to Google search for has_one vs. belongs_to reveals that the concept...
Related posts brought to you by Yet Another Related Posts Plugin.





