[譯] 使用Rails 4.2+ 測試異步郵件系統

異步測試老是一個很大的問題,郵件發送測試更是讓不少開發同窗不知道從哪裏入手。在新版的Rails裏,這類測試在很大程度上被簡化了。html

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

如下爲譯文sass


在編寫須要發送郵件的應用時,控制器是毫不能被阻塞的,所以異步發送必不可少。爲了實現這個途徑,郵件發送代碼必須從 request/response 週期轉移到能夠在後臺異步處理的進程中去。ruby

那麼,如此處理以後,代碼的正常運行又改如何保障?本篇博文中,咱們將重點關注測試的途徑,同時將會使用 MiniTest(Rails 已經內置了這個框架),可是使用的理念卻能夠很簡單地轉換爲 Rspec。app

如今有一個好消息,那就是從 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能夠幫助用戶大幅度避免隊列配置細節,在ResqueDelayed Job或其餘工做上也可使用。所以,若是咱們轉而使用 Sucker Punch,惟一的改變就是在引用相應的依賴包後,將queue_adapter從 :sidekiq 改成:sucker_punch 就能夠了。async

站在 Active Job 的肩膀上

若是你對 Rails 4.2 或者 Active Job 不太瞭解,https://blog.engineyard.com/2014/getting-started-with-active-job 能夠幫助你開始。然而,這篇文章留給個人一個小期許是,找到一種簡潔、地道的測試方法,從而讓全部組件都能正常的運行。編程語言

根據本文的目標,咱們假定已經部署了:ide

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

任何郵件都應該可以按照這裏描述的方式正常工做,這裏咱們就用一封歡迎郵件來使這個例子更實用:

#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

雖然咱們的測試是實時發生的,但在開篇就已經肯定毫不阻攔控制器,郵件發送之後臺任務進行,咱們如今須要部署全部組件以確保系統是肯定的。所以,在異步的世界裏,咱們必須先執行全部隊列中的任務才能評定他們的結果。爲了執行等待中的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中請求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 官方技術博客

關於譯者:李哲,OneAPM 工程師,擁有7年一線開發經驗,曾在大型民航、電力等企業就任,厭倦了國有企業的無聊氛圍以後,義無反顧的投進互聯網企業的大潮之中。平時喜歡研究各類編程語言,目前在 OneAPM 負責 Ruby 探針的研發,研究 Ruby 語言實現,以及 RubyVM 底層的技術。

相關文章
相關標籤/搜索