V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
OneAPM
V2EX  ›  Ruby on Rails

使用 Rails 4.2+ 测试异步邮件系统

  •  6
     
  •   OneAPM · 2015-06-01 10:58:55 +08:00 · 3092 次点击
    这是一个创建于 3224 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Alt text
    众所周知,假设想写一个需要发送邮件的应用,我们绝不能阻拦控制器,因此异步传送才是解决之道。为了达到这个目的,我们需要通过能在后台处理任务的异步处理进程库,将邮件发送代码从最初的request/response循环中移出。

    然而,做出这样的改变之后,我们如何确保代码能够一如往常的运行呢?在这篇博文中,我们会探索一种新方法来进行测试,就是我们将要使用的MiniTest测试框架(因为这也是Rails的选择),but the concepts presented here can be easily translated to RSpec。

    现在有一个好消息,那就是从Rails 4.2开始,异步传送邮件已经比之前简单多了。我们在例子中使用Sidekiq作为队列系统。但由于ActionMailer#deliver_later建立在ActiveJob之上,the interface is clean and agnostic of the asynchronous processing library used。这表示,要不是我刚才提了一下,身为开发者或用户的你也不会知情。其实,建立队列系统是一个独立的话题,你可以在开始使用Active Job中读到更多的信息。

    别太依赖小组件

    在例子中,我们假定Sidekiq及其依赖组件配置正确,因此本场景特有的一段代码是声明Active Job该使用哪一个队列调节器。

    # config/application.rb
    
    module OurApp
      class Application < Rails::Application
        …
        config.active_job.queue_adapter = :sidekiq
      end
    end
    

    Active Job在隐藏实质性的队列配置细节方面功能非常强大,以至于若是使用ResqueDelayed Job或其他组件,代码也不需要太大的改动。因此,如果我们转而使用Sucker Punch,唯一的改变就是在引用相应的依赖包后,将队列调节器从from :sidekiq改为 :sucker_punch

    站在Active Job的肩膀上

    如果你是Rails 4.2新手,或者对Active Job不太了解,Ben Lewis的Active Job介绍是就是很好的入门读物。然而,这篇文章留给我的一个小期许是,找到一种简洁、地道的测试方法,从而让所有组件都能正常的运行。

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

    • Rails 4.2或者一个更高的版本
    • Active Job set up to use a queueing backend (e.g. Sidekiq, Resque, etc.)
    • 一个邮件程序

    Any Mailer should work with the concepts described here,but we'll use this welcome email to make keep our examples pragmatic:

    #app/mailers/user_mailer.rb
    
    class UserMailer < ActionMailer::Base
      default from: '[email protected]'
    
      def welcome_email(user:)
        mail(
          to: user.email,
          subject: "Hi #{user.first_name}, and welcome!"
        )
      end
    end
    

    为了保持程序简单并有针对性,我们会为每个注册的用户发送一封欢迎邮件。

    这与Rails指南中的邮件系统案例如出一辙:

    # 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_jobs][assert-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
    

    虽然我们的测试是实时发生的,但在开篇就已经确定绝不阻拦控制器,邮件发送以后台任务进行,我们现在需要部署所有组件以确保系统是确定的。因此,在异步的世界里,我们必须先执行所有队列中的任务才能评定他们的结果。为了执行等待中的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测试指南建议在此阶段使用固定部署,但是我觉得他们太过脆弱。尤其是一开始,当我们还在试验设计或邮件内容,一个变化很快就会让他们变得过时,让我们的测试无法通过。

    我个人转而偏向使用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中请求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大大简化了设定工作基础环境的流程,你可以对自己所用的队列系统知之甚少。希望这篇教程能让你对此过程有更多了解。

    本文作者系OneAPM工程师编译整理。OneAPM是中国基础软件领域的新兴领军企业。专注于提供下一代应用性能管理软件和服务,帮助企业用户和开发者轻松实现:缓慢的程序代码和SQL语句的实时抓取。想阅读更多技术文章,请访问OneAPM官方技术博客

    3 条回复    2015-06-01 14:36:08 +08:00
    zhyu
        1
    zhyu  
       2015-06-01 12:56:45 +08:00
    全文转载不好吧
    另外个人意见,译文还是在标题写清楚比较好,而且还是中英混杂,没译完么。。
    aksoft
        2
    aksoft  
       2015-06-01 14:15:48 +08:00
    看着有点费劲...
    OneAPM
        3
    OneAPM  
    OP
       2015-06-01 14:36:08 +08:00
    @zhyu 感谢您的建议,我们下次会在标题里加上译文的注明。
    @aksoft 欢迎提出改进的意见 :)
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2936 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 11:09 · PVG 19:09 · LAX 04:09 · JFK 07:09
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.