ActionMailer, unit testing and multipart mails


#1

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.to = ‘’
@expected.subject = ‘’
@expected.content_type = ‘multipart/mixed; boundary=“something”’

body_part = TMail::Mail.new
body_part.content_type = 'text/plain'
body_part.body         = read_fixture('notification')
@expected.parts << body_part      # <=== ERROR HERE

attach_part = TMail::Mail.new
attach_part.content_type = 'application/octet-stream'
attach_part.encoding     = 'base64'
attach_part.body         = 'abc'
@expected.parts << 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:inread_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:inparse_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:inropen’
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:inparts’
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.

TX


#2

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

TX


#3

if you commented out this line (and any other line that squawks)…

@expected.parts << body_part      # <=== ERROR HERE

and printed out the mail’s parts before asserting them…

mail = Notifier.create_notification()
p mail.parts

…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…


Phlip


#4

On Dec 12, 12:27 pm, Phlip removed_email_address@domain.invalid wrote:

…what would you see?
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.

TX


#5

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.

TX