異步測試老是一個很大的問題,郵件發送測試更是讓不少開發同窗不知道從哪裏入手。在新版的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
能夠幫助用戶大幅度避免隊列配置細節,在Resque
、Delayed Job
或其餘工做上也可使用。所以,若是咱們轉而使用 Sucker Punch,惟一的改變就是在引用相應的依賴包後,將queue_adapter
從 :sidekiq 改成:sucker_punch 就能夠了。async
若是你對 Rails 4.2 或者 Active Job 不太瞭解,https://blog.engineyard.com/2014/getting-started-with-active-job 能夠幫助你開始。然而,這篇文章留給個人一個小期許是,找到一種簡潔、地道的測試方法,從而讓全部組件都能正常的運行。編程語言
根據本文的目標,咱們假定已經部署了:ide
任何郵件都應該可以按照這裏描述的方式正常工做,這裏咱們就用一封歡迎郵件來使這個例子更實用:
#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
接下來,咱們想確保控制器內的任務能如所期待的那樣執行。
在測試指南中,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 底層的技術。