ActionMailer es la librería incluída en Ruby on Rails para gestionar el envío de correos desde el framework.

Su uso es bastante sencillo y cumple su labor perfectamente, a excepción de pequeños detalles que parece que falten por pulir.

Uno de esos "detalles insignificantes" es, para mi humilde opinión, el testing, que, aunque se puede realizar sin problemas, y con una cobertura muy buena de la funcionalidad, no queda tan limpio como queda en otras partes del framework.

De hecho, una de las cosas que más se echa en falta es algo de documentación. Por suerte, en los Manuales de Rails (qué mal suena, ¿no?), encontramos un "libro" sobre testing en Rails, y uno de sus capítulos es testing your Mailers, que es justo lo que queremos hacer.

Ahí proponen un esquema o esqueleto para los tests de unidad tal que así:


 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
 

Así un poco por encima, además de los requires pertinentes y de establecer unas cuantas constantes debemos de incluír include ActionMailer::Quoting para poder utilizarlo en los tests con aquellos emails que tengan acentos, eñes o cualquier otro carácter no estándar en el subject.

En el método setup configuramos el ActionMailer y además creamos un objeto TMail llamado @expected, y que utilizaremos en todos los tests para compararlo con el que genera nuestro modelo:


   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
 

Y respecto a las fixturas, básicamente no tenemos, así que nos proponen utilizar ficheros de texto plano cuyo contenido será el contenido del email. Esos ficheros se sitúan en RAIlS_ROOT/test/fixtures/community_mailer/ como se puede ver en la función read_fixture:


   def read_fixture(action)
     IO.readlines("#{FIXTURES_PATH}/community_mailer/#{action}")
   end
 

Nuestro propio test

Con el esquema ya creado podemos empezar a escribir nuestros propios tests sobre nuestros modelos de Mailer.

Por ejemplo, veamos una acción muy simple de La Coctelera, que te envía un email cada vez que alguien te añade como amigo:


 	def new_friend(user,friend)
     @headers['reply-to'] = user.email
     @from = 'admin@domain.com'
     @recipients = friend.email
     @subject = "[La Coctelera] #{user.shortname} te ha añadido como amigo"
     @body = {
       'user_name' => user.name,
       'friend_name' => friend.name,
       'profile_link' => profile_url(user.username),
       'profile_link_friends' => friends_url(friend.username)
     }
   end
 

Y esta es la vista asociada:


 Hola <%= @friend_name %>!
 
 <%= @user_name %> te ha añadido como amig@. Puedes visitar su página en la siguiente
 dirección: <%= @profile_link %>
 
 Puedes ver a todos tus amigos en:
 
 <%= @profile_link_friends %>
 
 Hasta otra!
 
 --
 La Coctelera
 http://www.lacoctelera.com
 

El test podría ser algo tal que así:


 def test_new_friend
   user = User.find_by_username('lucas')
   friend = User.find_by_username('blat')
   @expected.from = APP['adminmaster']
   @expected.subject = encode("[La Coctelera] #{user.shortname} te ha añadido como amigo")
   @expected.body = read_fixture('new_friend')
   @expected.to = friend.email
   @expected.reply_to = user.email
   assert_equal @expected.encoded, Notifier.create_new_friend(user, friend).encoded
 end
 

El procedimiento es muy sencillo:

  • cargamos a los dos usuarios implicados en la acción
  • vamos rellenando los atributos de @expected con los valores que esperamos que tenga el email resultante: remitente, asunto, contenido, destinatario...
  • finalmente, comparamos el email "esperado", con el que genera la acción create_new_friend

Hemos necesitado crear una fixtura new_friend con el siguiente contenido:


 Hola Fernando!
 
 Lucas Nicolás te ha añadido como amig@. Puedes visitar su página en la siguiente dirección: http://test.host/lucas/perfil
 
 Puedes ver a todos tus amigos en:
 
 http://test.host/blat/amigos
 
 Hasta otra!
 
 --
 La Coctelera
 http://test.host
 

Como veis, esta fixtura tiene una limitación más que evidente: no es dinámica, y su contenido depende de los valores (en este caso de los usuarios), que estamos utilizando en el test.

También quiero destacar esta línea:


   @expected.subject = encode("[La Coctelera] #{user.shortname} te ha añadido como amigo")
 

Cuando el subject del email vaya a incluir algún caracter acentuado, o con eñe, como es el caso, hay que forzar la recodificación de la cadena, pues ActionMailer también la recodificará cuando cree el email.

En un post posterior propondremos un par de mejoras para solventar alguna de las deficiencias que hemos visto.