您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
使用Rails 4.2+ 测试异步邮件系统
 
作者 EngineYard 来源:EngineYard 火龙果软件  发布于 2015-06-01
  2946  次浏览      50
 

摘要:在编写需要发送邮件的应用时,控制器是绝不能被阻塞的,因此异步发送必不可少。为了实现这个途径,邮件发送代码必须从request/response周期转移到可以在后台异步处理的进程中去。

【编者按】异步测试总是一个很大的问题,邮件发送测试更是让很多开发同学不知道从哪里入手。在新版的Rails里,这类测试在很大程度上被简化了。

关于译者:李哲,OneAPM工程师,拥有7年一线开发经验,曾在大型民航、电力等企业就职,厌倦了国有企业的无聊氛围之后,义无反顾的投进互联网企业的大潮之中。平时喜欢研究各种编程语言,目前在OneAPM负责Ruby探针的研发,研究Ruby语言实现,以及RubyVM底层的技术。

以下为译文

在编写需要发送邮件的应用时,控制器是绝不能被阻塞的,因此异步发送必不可少。为了实现这个途径,邮件发送代码必须从request/response周期转移到可以在后台异步处理的进程中去。

那么,如此处理之后,代码的正常运行又改如何保障?本篇博文中,我们将重点关注测试的途径,同时将会使用MiniTest(Rails已经内置了这个框架),但是使用的理念却可以很简单地转换为Rspec。

现在有一个好消息,那就是从Rails 4.2开始,异步邮件发布已经比之前简单多了。我们在例子中使用Sidekiq作为队列系统。由于ActionMailer#deliver_later建立在ActiveJob之上,接口非常的简洁明了。这表示,要不是我刚才提了一下,身为开发者或用户的你也不会知情。建立队列系统是另外一个话题,你可以在getting started with Active Job here中了解更多详细信息。

别太依赖小组件

在例子中,假设你已经正确配置了Sidekiq及其依赖组件,因此本场景中唯一特有的代码就是申明Active Job该使用哪一个队列调节器。

# config/application.rb

module OurApp
class Application < Rails::Application
# ...
config.active_job.queue_adapter = :sidekiq
end
end

Active Job可以帮助用户大幅度避免队列配置细节,在Resque、Delayed Job或其他工作上也可以使用。因此,如果我们转而使用Sucker Punch,唯一的改变就是在引用相应的依赖包后,将queue_adapter从:sidekiq改为:sucker_punch就可以了。

站在Active Job的肩膀上

如果你对Rails 4.2或者Active Job不太了解,https://blog.engineyard.com/2014/getting-started-with-active-job可以帮助你开始。然而,这篇文章留给我的一个小期许是,找到一种简洁、地道的测试方法,从而让所有组件都能正常的运行。

根据本文的目标,我们假定已经部署了:

  • Rails 4.2或者一个更高的版本
  • 已经设置好queue_adapter的Active Job (Sidekiq、Resque等)
  • 一封邮件

任何邮件都应该能够按照这里描述的方式正常工作,这里我们就用一封欢迎邮件来使这个例子更实用:

#app/mailers/user_mailer.rb

class UserMailer < ActionMailer::Base
default from: 'email@example.com'

def welcome_email(user:)
mail(
to: user.email,
subject: "Hi #{user.first_name}, and welcome!"
)
end
end

为了保持程序简单并有针对性,这里会在每个用户注册后发送给他们一封欢迎邮件。

这和the Rails guides mailer example是一样的:

# app/controllers/users_controller.rb

class UsersController < ApplicationController
def create
# Yes, Ruby 2.0+ keyword arguments are preferred
UserMailer.welcome_email(user: @user).deliver_later
end
end

The Mailer Should Do Its Job, Eventually

接下来,我们想确保控制器内的任务能如所期待的那样执行。

在测试指南中,custom assertions for testing jobs inside other components的章节介绍了大约六种这样的自定义断言方法。

或许直觉告诉你应该单刀直入,然后使用assert_enqueued_jobsassert-enqueued-jobs来测试每次添加新用户时,我们有否将邮件传送任务放入队列。

你可能会这么做:

# test/controllers/users_controller_test.rb

require 'test_helper'

class UsersControllerTest < ActionController::TestCase
test 'email is enqueued to be delivered later' do
assert_enqueued_jobs 1 do
post :create, {}
end
end
end

然而如果这么做,你会惊奇地发现测试失败了,系统会告诉你assert_enqueued_jobs未经定义,且无法使用。

这是因为,我们的测试类继承自ActionController::TestCase,而后者在编写时没有包含ActiveJob::TestHelper。

不过我们很快就可以修正这一点:

# test/test_helper.rb

class ActionController::TestCase
include ActiveJob::TestHelper
end

假定我们的代码如期执行,那么测试应该就能顺利通过了。

这是好消息。现在,我们可以重构我们的代码,增加新的功能,也可以增加新的测试。我们可以选择后者,看看我们的邮件有否投递成功,如果是的话,那就检查投递的内容是否正确。

ActionMailer能为我们提供一个包含所有发出邮件的队列,前提是将delivery_method选项设置为:test,我们能通过ActionMailer::Base.deliveries读取这个队列。

在同步投递邮件时,检测邮件是否发送成功是很容易的。我们只需检查在动作完成后,投递计数器加1。用MiniTest来写的话,就像下面这样:

assert_difference 'ActionMailer::Base.deliveries.size', +1 do
post :create, {}
end

我们的测试是实时发生的,但在开篇就已经知道不能阻拦控制器,需要在后台进程中发送邮件,现在我们把所有的组件都组装起来,确定系统是没有问题的。因此,在异步的世界里,我们必须先执行所有队列中的任务再去判定执行结果。为了执行pending中的Active Job任务,我们使用perform_enqueued_jobs:

test 'email is delivered with expected content' do
perform_enqueued_jobs do
post :create, {}
delivered_email = ActionMailer::Base.deliveries.last

# assert our email has the expected content, e.g.
assert_includes delivered_email.to, @user.email
end
end

缩短反馈流程

目前为止,我们都在进行功能性测试以确保我们的控制器如期执行。但是,代码的变化足以破坏我们发送的邮件,为什么不对我们的邮件程序进行单元测试,从而缩短反馈流程,然后更快地洞察变化呢?

Rails测试指南建议在这里使用fixtures,但是我觉得他们太生硬了。尤其是一开始,当我们还在尝试设计邮件时,快速变化就会让他们变得不可用,让我们的测试无法通过。我偏向使用assert_match以关注那些构成邮件主体的关键元素。

为此,也因为其他原因(比如抽离处理多部分邮件的逻辑结构),我们可以建立自定义断言。这可以扩展MiniTest标准断言或Rails专属断言。这也是创建自己的领域专属语言(Domain Specific Language)并用于测试的好例子。

让我们在测试一文件夹内创建一个共享文件夹,用以存放SharedMailerTests模块。我们自定义的断言可以这么来写:

# /test/shared/shared_mailer_tests.rb module 
SharedMailerTests def assert_email_body_matches(matcher:,
email:) if email.multipart? %w(text html).each
do |part| assert_match matcher,
email.send("#{part}_part").body.to_s end else
assert_match matcher, email.body.to_s
end end end

接下来,我们需要让邮件测试系统注意到这个自定义断言,为此,我们可以将其放入ActionMailer::TestCase类中。然后可以借鉴之前把ActiveJob::TestHelper类包含于ActionController::TestCase类的方法:

# test/test_helper.rb

require 'shared/shared_mailer_tests'

class ActionMailer::TestCase
include SharedMailerTests
end

注意,我们首先需要在test_helper中require shared_mailer_tests。

这些办好之后,我们现在可以确信我们的邮件中包含我们期望的关键元素。假设我们想确保发送给用户的URL包含一些用于追踪的特定UTM参数。我们现在可以将自定义断言与老朋友perform_enqueued_jobs联合起来使用,就像这样:

# test/mailers/user_mailer_test.rb

class ToolMailerTest < ActionMailer::TestCase
test 'emailed URL contains expected UTM params' do
UserMailer.welcome_email(user: @user).deliver_later

perform_enqueued_jobs do
refute ActionMailer::Base.deliveries.empty?

delivered_email = ActionMailer::Base.deliveries.last
%W(
utm_campaign=#{@campaign}
utm_content=#{@content}
utm_medium=email
utm_source=mandrill
).each do |utm_param|
assert_email_body_matches utm_param, delivered_email
end
end
end

结论

在Active Job的基础上,使用ActionMailer让从同步发送邮件到通过队列发送邮件的转化变得如此简单,就如同从deliver_now转化到deliver_later。

同时,由于使用Active Job大大简化了设定工作基础环境的流程,你可以对自己所用的队列系统知之甚少。希望这篇教程能让你对此过程有更多了解。

英文原文:Testing async emails, the Rails 4.2+ way

 

   
2946 次浏览       50
 
相关文章

云计算的架构
对云计算服务模型
云计算核心技术剖析
了解云计算的漏洞
 
相关文档

云计算简介
云计算简介与云安全
下一代网络计算--云计算
软浅析云计算
 
相关课程

云计算原理与应用
云计算应用与开发
CMMI体系与实践
基于CMMI标准的软件质量保证
最新课程计划

专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件的思考
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS
更多...   
相关培训课程

云计算原理与应用
Windows Azure 云计算应用

摩托罗拉 云平台的构建与应用
通用公司GE Docker原理与实践
某研发中心 Openstack实践
知名电子公司 云平台架构与应用
某电力行业 基于云平台构建云服务
云计算与Windows Azure培训
北京 云计算原理与应用