ActionMailer, unit testing and multipart mails

Hi all.

Is there a "correct" way of writing a unit test for a mailer which sends attachments?

I tried using the @expected variable as provided in ActionMailer::TestCase, but it led to various problems.

Here's what I'm attempting...

  def test_notification     @expected.from = '' = ''     @expected.subject = ''     @expected.content_type = 'multipart/mixed; boundary="something"'

    body_part =     body_part.content_type = 'text/plain'     body_part.body = read_fixture('notification') << body_part # <=== ERROR HERE

    attach_part =     attach_part.content_type = 'application/octet-stream'     attach_part.encoding = 'base64'     attach_part.body = 'abc' << attach_part

    mail = Notifier.create_notification()

    assert_equal @expected.encoded, mail.encoded   end

This gives the following error:

TypeError: can't convert nil into String C:/ruby/lib/ruby/gems/1.8/gems/actionmailer-2.1.2/lib/action_mailer/ vendor/tmail-1.2.3/tmail/mail.rb:551:in `quote'     C:/ruby/lib/ruby/gems/1.8/gems/actionmailer-2.1.2/lib/ action_mailer/vendor/tmail-1.2.3/tmail/mail.rb:551:in `read_multipart'     C:/ruby/lib/ruby/gems/1.8/gems/actionmailer-2.1.2/lib/ action_mailer/vendor/tmail-1.2.3/tmail/mail.rb:540:in `parse_body_0'     C:/ruby/lib/ruby/gems/1.8/gems/actionmailer-2.1.2/lib/ action_mailer/vendor/tmail-1.2.3/tmail/mail.rb:526:in `parse_body'     C:/ruby/lib/ruby/gems/1.8/gems/actionmailer-2.1.2/lib/ action_mailer/vendor/tmail-1.2.3/tmail/stringio.rb:43:in `open'     C:/ruby/lib/ruby/gems/1.8/gems/actionmailer-2.1.2/lib/ action_mailer/vendor/tmail-1.2.3/tmail/port.rb:340:in `ropen'     C:/ruby/lib/ruby/gems/1.8/gems/actionmailer-2.1.2/lib/ action_mailer/vendor/tmail-1.2.3/tmail/mail.rb:524:in `parse_body'     C:/ruby/lib/ruby/gems/1.8/gems/actionmailer-2.1.2/lib/ action_mailer/vendor/tmail-1.2.3/tmail/mail.rb:497:in `parts'     C:/Projects/portal/trunk/test/unit/notifier_test.rb:16:in `test_notification'

Line 16 is commented in the above code. Looking at mail.rb:551 suggests that body is nil, and I can confirm that the body passed in was not nil.

I know I can just not use @expected and check each bit of the mail separately, but I figure @expected is there for a reason (and also, checking each bit of the mail only verifies what is supposed to be there, not what isn't.)

Maybe I'm just doing things completely wrong, as I'm not 100% used to Rails 2.x yet.


So... nobody is doing mails with attachments? Or anyone who is, isn't unit testing their code?


if you commented out this line (and any other line that squawks)... << body_part # <=== ERROR HERE

and printed out the mail's parts before asserting them...

    mail = Notifier.create_notification()


...what would you see?

We TDD e-mails all the time, but we don't write huge bulk assertions. Sometimes to test_first_, we cheat a little, write the correct code, print out what it does, and then write assertions which trap the important details.

To test-first, when you need to change a mail, you clone that test, change the input, change the assertions to expect different output, fail the test, then pass it in the code. This shows how cheating, especially in high-bandwidth situations like GUIs, can lead to better TDD...

It looks like this...

[#<TMail::Mail port=#<TMail::StringPort:id=0x2ea1e18> bodyport=#<TMail::StringPort:id=0x2ea1a12>>, #<TMail::Mail port=#<TMail::StringPort:id=0x2ea1486> bodyport=#<TMail::StringPort:id=0x2ea0f68>>]

Something I have just discovered today. If you delay setting content_type to *after* adding the parts, it gets rid of that exception. Now it appears to be at least not causing an error, but I get a failure because (of course) the content types are subtly different.

What's truly bizarre about it is that the MIME boundary string is quoted for the actual mail but not quoted for the expected mail, even though it doesn't need to be quoted in either case. TMail seems to be doing some weird things there.

I think if I make the test environment overwrite TMail's new_boundary method, it might give me a way to dodge that.


Okay, solution found. Requires a combination of two tricks:

1. content_type has to be set after the parts, as you get the error above otherwise. Maybe a bug in TMail, who knows. At least there is a workaround.

2. Boundary strings need mangling. I created myself a convenience method which will ultimately end up in my test helper if I have more than one notification model later.

  def assert_mail_equal(expected_mail, actual_mail)     assert_equal replace_boundary_strings(expected_mail.encoded),                  replace_boundary_strings(actual_mail.encoded)   end

  def replace_boundary_strings(str)     boundary_pattern = 'mimepart_[0-9a-f]+_[0-9a-f]+'     str = str.gsub(/(Content-Type: multipart\/mixed; boundary=)\"?# {boundary_pattern}\"?/, "\\1replaced")     str = str.gsub(/--#{boundary_pattern}/, "--replaced")   end

I had suspected there was a second solution in tricking TMail into generating the same boundary string on calls to new_boundary, but I couldn't get that to work.