Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 1st Edition (2005).pdf
Скачиваний:
28
Добавлен:
17.08.2013
Размер:
7.99 Mб
Скачать

RECEIVING E-MAIL 406

Figure 19.1: An HTML-Format E-mail

 

And finally we’ll test this using an action method that renders the e-mail,

 

sets the content type to text/html, and calls the mailer to deliver it.

File 142

class TestController < ApplicationController

 

def ship_order

 

order = Order.find_by_name("Dave Thomas")

 

email = OrderMailer.create_sent(order)

 

email.set_content_type("text/html")

 

OrderMailer.deliver(email)

 

render(:text => "Thank you...")

 

end

 

end

 

The resulting e-mail will look something like Figure 19.1 .

19.2 Receiving E-mail

Action Mailer makes it easy to write Rails applications that handle incoming e-mail. Unfortunately, you also need to find a way of getting appropriate e-mails from your server environment and injecting them into the application; this requires a bit more work.

The easy part is handling an e-mail within your application. In your Action Mailer class, write an instance method called receive( ) that takes a single parameter. This parameter will be a TMail::Mail object corresponding to the incoming e-mail. You can extract fields, the body text, and/or attachments and use them in your application.

For example, a bug tracking system might accept trouble tickets by e-mail.

Prepared exclusively for Rida Al Barazi

Report erratum

RECEIVING E-MAIL 407

From each e-mail, it constructs a Ticket model object containing the basic ticket information. If the e-mail contains attachments, each will be copied into a new TicketCollateral object, which is associated with the new ticket.

File 143

class IncomingTicketHandler < ActionMailer::Base

 

def receive(email)

 

ticket = Ticket.new

 

ticket.from_email = email.from[0]

 

ticket.initial_report = email.body

 

if email.has_attachments?

email.attachments.each do |attachment| collateral = TicketCollateral.new(

:name

=>

attachment.original_filename,

:body

=>

attachment.read)

ticket.ticket_collaterals << collateral end

end ticket.save

end end

So now we have the problem of feeding an e-mail received by our server computer into the receive( ) instance method of our IncomingTicketHandler. This problem is actually two problems in one: first we have to arrange to intercept the reception of e-mails that meet some kind of criteria, and then we have to feed those e-mails into our application.

If you have control over the configuration of your mail server (such as a Postfix or sendmail installation on Unix-based systems), you might be able to arrange to run a script when an e-mail address to a particular mailbox or virtual host is received. Mail systems are complex, though, and we don’t have room to go into all the possible configuration permutations here. There’s a good introduction to this on the Ruby development Wiki at http://wiki.rubyonrails.com/rails/show/HowToReceiveEmailsWithActionMailer.

If you don’t have this kind of system-level access but you are on a Unix system, you could intercept e-mail at the user level by adding a rule to your .procmailrc file. We’ll see an example of this shortly.

The objective of intercepting incoming e-mail is to pass it to our application. To do this, we use the Rails runner facility. This allows us to invoke code within our application’s code base without going through the web. Instead, the runner loads up the application in a separate process and invokes code that we specify in the application.

All of the normal techniques for intercepting incoming e-mail end up running a command, passing that command the content of the e-mail as standard input. If we make the Rails runner script the command that’s invoked whenever an e-mail arrives, we can arrange to pass that e-mail into our

Prepared exclusively for Rida Al Barazi

Report erratum

TESTING E-MAIL 408

application’s e-mail handling code. For example, using procmail-based interception, we could write a rule that looks something like the example that follows. Using the arcane syntax of procmail, this rule copies any incoming e-mail whose subject line contains Bug Report through our runner script.

RUBY=/Users/dave/ruby1.8/bin/ruby

TICKET_APP_DIR=/Users/dave/Work/BS2/titles/RAILS/Book/code/mailer

HANDLER='IncomingTicketHandler.receive(STDIN.read)'

:0 c

* ^Subject:.*Bug Report.*

| cd $TICKET_APP_DIR && $RUBY script/runner $HANDLER

The receive( ) class method is available to all Action Mailer classes. It takes the e-mail text passed as a parameter, parses it into a TMail object, creates a new instance of the receiver’s class, and passes the TMail object to the receive( ) instance method in that class. This is the method we wrote on page 406. The upshot is that an e-mail received from the outside world ends up creating a Rails model object, which in turn stores a new trouble ticket in the database.

19.3 Testing E-mail

There are two levels of e-mail testing. At the unit test level you can verify that your Action Mailer classes correctly generate e-mails. At the functional level, you can test that your application sends these e-mails when you expect it to.

Unit Testing E-mail

When we used the generate script to create our order mailer, it automatically constructed a corresponding order_mailer_test.rb file in the application’s test/unit directory. If you were to look at this file, you’d see that it is fairly complex. That’s because it tries to arrange things so that you can read the expected content of e-mails from fixture files and compare this content to the e-mail produced by your mailer class. However, this is fairly fragile testing. Any time you change the template used to generate an e-mail you’ll need to change the corresponding fixture.

If exact testing of the e-mail content is important to you, then use the pregenerated test class. Create the expected content in a subdirectory of the test/fixtures directory named for the test (so our OrderMailer fixtures would be in test/fixtures/order_mailer). Use the read_fixture( ) method included in the generated code to read in a particular fixure file and compare it with the e-mail generated by your model.

Prepared exclusively for Rida Al Barazi

Report erratum

TESTING E-MAIL 409

However, I prefer something simpler. In the same way that I don’t test every byte of the web pages produced by templates, I won’t normally bother to test the entire content of a generated e-mail. Instead, I test the thing that’s likely to break: the dynamic content. This simplifies the unit test code and makes it more resilient to small changes in the template. Here’s a typical e-mail unit test.

File 151

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

 

require 'order_mailer'

 

class OrderMailerTest < Test::Unit::TestCase

def setup

@order = Order.new(:name =>"Dave Thomas", :email => "dave@pragprog.com") end

def test_confirm

response = OrderMailer.create_confirm(@order) assert_equal("Pragmatic Store Order Confirmation", response.subject) assert_equal("dave@pragprog.com", response.to[0]) assert_match(/Dear Dave Thomas/, response.body)

end end

The setup( ) method creates an order object for the mail sender to use. In the test method we get the mail class to create (but not to send) an e-mail, and we use assertions to verify that the dynamic content is what we expect. Note the use of assert_match( ) to validate just part of the body content.

Functional Testing of E-mail

Now that we know that e-mails can be created for orders, we’d like to make sure that our application sends the correct e-mail at the right time. This is a job for functional testing.

Let’s start by generating a new controller for our application.

depot> ruby script/generate controller Order confirm

We’ll implement the single action, confirm, which sends the confirmation e-mail for a new order.

File 141

class

OrderController < ApplicationController

 

def

confirm

 

order = Order.find(params[:id])

OrderMailer.deliver_confirm(order) redirect_to(:action => :index)

end

end

We saw how Rails constructs a stub functional test for generated controllers back in Section 12.3, Testing Controllers, on page 148. We’ll add our mail testing to this generated test.

Prepared exclusively for Rida Al Barazi

Report erratum

TESTING E-MAIL 410

Action Mailer does not deliver e-mail in the test environment. Instead, it appends each e-mail it generates to an array, ActionMailer::base.deliveries. We’ll use this to get at the e-mail generated by our controller. We’ll add a couple of lines to the generated test’s setup( ) method. One line aliases this array to the more manageable name @emails. The second clears the array at the start of each test.

File 150

@emails

= ActionMailer::Base.deliveries

 

@emails.clear

We’ll also need a fixture holding a sample order. We’ll create a file called orders.yml in the test/fixtures directory.

File 149

daves_order:

 

id:

1

 

name:

Dave Thomas

 

address:

123 Main St

 

email:

dave@pragprog.com

Now we can write a test for our action. Here’s the full source for the test class.

File 150

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

 

require 'order_controller'

 

# Re-raise errors caught by the controller.

 

class OrderController; def rescue_action(e) raise e end; end

 

class OrderControllerTest < Test::Unit::TestCase

fixtures :orders

def setup

 

 

@controller =

OrderController.new

@request

=

ActionController::TestRequest.new

@response

=

ActionController::TestResponse.new

@emails

=

ActionMailer::Base.deliveries

@emails.clear end

def test_confirm

get(:confirm, :id => @daves_order.id) assert_redirected_to(:action => :index) assert_equal(1, @emails.size)

email = @emails.first

assert_equal("Pragmatic Store Order Confirmation", email.subject) assert_equal("dave@pragprog.com", email.to[0]) assert_match(/Dear Dave Thomas/, email.body)

end end

It uses the @emails alias to access the array of e-mails generated by Action Mailer since the test started running. Having checked that exactly one e-mail is in the list, it then validates the contents are what we expect.

We can run this test either by using the test_functional target of rake or by executing the script directly.

depot> ruby test/functional/order_controller_test.rb

Prepared exclusively for Rida Al Barazi

Report erratum