建築釺


為異步工作負載編寫有效的集成測試

通過馬克斯Gurewitz2021年12月6日

當軟件公司的業務和軟件達到足夠的複雜程度時,它必然需要找到一種有效處理異步工作負載的方法。這些工作負載將經常由異步作業處理框架處理,例如Ruby的SidekiqPython的Redis隊列(RQ),工作人員從RabbitMQ或者使用AWS Lambda函數從Amazon Simple Queue Service (SQS)讀取數據。但是,盡管這種需求經常出現,開發人員通常缺乏為這些異步工作負載構建有效集成測試的經驗。

在Braze,我們為異步工作負載編寫集成測試已經有很多年了。隨著時間的推移,我們已經學習了很多關於如何以適合我們的體係結構並支持我們的工程工作的深思熟慮的方式來解決這個關鍵需求的方法。根據這些經驗,我們將:

  • 了解如何編寫測試以確保後台作業更新是向後兼容的。

  • 考慮使用編組作為作業參數編碼的危險。

我們開始吃吧。

與測試異步工作負載相關的常見挑戰

1.多步驟工作流程

作為一項規則,異步作業通常不是孤立運行的。相反,它們往往被組合成一個作業圖,然後按順序執行。在這種情況下,對單個作業的行為進行單元測試不一定能告訴您太多信息。用集成測試測試整個工作流程的最終行為更有效。

2.異步的期望

編寫針對同步API(如HTTP服務器)的行為斷言的測試的過程往往相當簡單:傳遞API參數,然後斷言響應具有某些特定的特征集。但是,在測試異步API(如作業處理器)時,此選項不可用,因為這會導致更複雜、更具挑戰性的流程。

3.外部依賴關係

在許多情況下,異步工作負載需要訪問外部第三方服務才能正常工作。但是,在測試中聯係這些服務可能會導致問題。首先,它們可以通過引起測試結果的交叉汙染,將不確定性引入到您的測試中。針對真正的外部服務進行測試也可能是不安全的,例如,如果該服務的API產生了不應該在生產之外觸發的副作用。在實際層麵上,還應該記住,外部服務可能非常昂貴,因此將它們用於測試可能會導致不必要的、浪費的成本。

還有其他的問題嗎?因為我們正在編寫一個集成測試,它將涉及多個進程,所以不可能使用傳統的存根策略來利用Ruby的RSpec、Python的unittest、Javascript的Jest或Java的Junit等庫。

與在Braze上測試異步工作負載相關的挑戰

雖然上麵列出的挑戰並不詳盡,但它確實涵蓋了Braze在測試我們的核心消息傳遞管道時所麵臨的幾個關鍵挑戰。

在Braze,我們在Sidekiq工人上運行Redis排隊的工作,這些工作的主要職責之一是使用Twilio或Sparkpost等外部服務向客戶的用戶發送消息(如電子郵件、短信)。每個作業都有條件地按順序將下一個作業排隊,這最終可以產生一個或多個已分派消息。在評估這些作業的最終行為時,我們希望通過測試來判斷作業向這些外部服務發出的請求的數量和內容。

在異步工作負載上構建測試需要什麼

在Braze,我們發現,通過包含以下組件,可以為異步工作負載編寫有效的集成測試。

1.隊列

作為此過程的一部分,您將需要一個隊列,用於對執行您試圖執行的期望行為的作業進行排隊。在Braze,這將是一個Redis實例,但根據你的設置,它可以是其他東西,如RabbitMQ應用程序;或者,如果您正在使用像SQS這樣的專有技術,那麼如果它提供了兼容的接口(例如ElasticMQ),您可能會利用一個開源的替代方案。

2.工人

要執行您的測試,您需要運行一個worker,它將監聽您的作業隊列。這個worker將包含您最終希望測試的代碼;例如,在Braze,我們會在這種情況下使用Sidekiq員工。

3.偽外部服務依賴關係

在某些情況下,您可能會發現,偽造外部服務依賴關係是有益的(或者是必要的),而不是將它們作為集成測試的一部分實際調用。您可以通過編寫一個應用程序來實現這一點,該應用程序通過簡化的實現來再現與工作人員用例相匹配的最小API。這個偽API將保存傳入請求的日誌;你所需要做的就是通過某種方式公開這個日誌,例如通過一個HTTP端點進行查詢和斷言。

對於Braze來說,這個組件是我們測試策略的關鍵部分,很大程度上是因為我們測試的最終行為涉及到我們向Twilio等外部服務發出的請求。為了做到這一點,我們創建了一個偽API,它能夠對這些服務進行抽象,然後在專用的“/request-log”端點上公開它收到的請求。

4.測試運行

通常,您還需要一個專門的進程來運行您的測試,我們將其稱為“測試運行器”。這些測試將在隊列中對作業進行排隊,然後可以輪詢偽造的外部服務(或工作人員的數據存儲)的請求日誌,以斷言工作人員在給定的超時時間內產生了所需的副作用。在Braze,這些測試在Redis中對作業進行排隊,然後輪詢我們的/請求日誌端點,斷言我們試圖在測試開始的幾秒鍾內聯係用戶。

(將作業庫導入測試運行器也很有用,這樣測試就可以根據需要同步執行作業。)

5.其他應用程序依賴關係

除了這些組件,你還需要運行工作人員所依賴的任何內部應用程序;這可能包括某種形式的數據存儲。在Braze,這些測試運行像MongoDB和Memcached這樣的數據存儲。

一個常見問題:向後不兼容的作業更新

雖然在異步作業流程中可能會出現許多問題,但最常見的故障模式之一(在Braze和其他地方)是對作業的參數進行了無意的向後不兼容更改。

在修改作業的參數時,一些作業通常會以較舊的格式在隊列中保留一段時間,從而導致隊列中混合使用作業參數格式。考慮到這一點,為了避免出現問題,這些作業的實現需要使工作人員能夠在一段時間內同時接受新格式和舊格式的參數。

結果呢?如果您對測試不深思熟慮和不一致,就很容易在作業的參數中無意地引入向後不兼容的更改。

編組和向後不兼容的風險

編組是一種常用的內存編碼格式。它通常是為了方便而使用的,因為使用編組很容易對複雜的數據結構進行編碼,否則可能需要額外的勞動以不同的格式進行編碼。然而,這種便利是有代價的。

作為一種規則,編組將編碼格式與應用程序代碼的結構捆綁在一起,這可能會產生潛在的問題。想象一下,您需要在您的工作者內部升級一個外部維護的庫,或者升級您的工作者的語言版本及其標準庫。如果其中一個庫的內部實現細節發生了變化,導致對象不能跨庫版本編組和解組,這種變化的一個影響將是它破壞了工作程序的向後兼容性(即導致它無法處理以以前的格式發出的作業,或以可以由以前的工作程序版本處理的格式對作業進行排隊)。

除非您非常熟悉其對象被編組的每個庫的實現,否則很難預測哪個庫升級會導致向後兼容性問題。鑒於此,在對有效負載進行編碼時,避免在默認情況下進行編組通常是一個好主意。

在Braze,我們目前使用Ruby編組來在Ruby對象的作業參數中編碼Ruby對象,我們已經通過艱苦的方式了解到,為作業參數編碼編組會增加引入向後不兼容的風險。在我們的例子中,我們在嚐試升級時遇到了這個問題Mongoid它是一個對象文檔映射器(ODM),為Ruby應用程序中的MongoDB提供類似活動記錄的功能。事實證明,在某些條件下,Mongoid無法在版本6和版本7之間馬歇爾和反馬歇爾模型,從而引入了意想不到的向後不兼容

(另外一個注意事項:Ruby on Rails目前使用編組編碼它的緩存有效負載,這使它容易出現上麵提到的問題。)

測試向後兼容性

通過對以新格式和舊格式編碼的作業參數進行排隊,可以成功地測試參數更改的向後兼容性。一旦你做到了這一點,你就可以驗證你的工作人員以兩種格式正確地處理作業。

減少編組的複雜性

引入編組作業參數編碼將使測試作業參數更改的向後兼容性的過程複雜化。編組可以將作業參數格式綁定到應用程序代碼庫的任意元素的結構上;因此,您將需要跨測試運行使用完全不同的應用程序構件,以有效地改變編組編碼的作業參數格式。

在Braze,我們用碼頭工人組成(及其支持環境變量)在安裝了Mongoid 6的Docker映像上運行一組測試,在安裝了Mongoid 7的Docker映像上運行另一組測試。采用這種方法使我們能夠再現我們在最初的產品Mongoid升級嚐試中遇到的解碼錯誤——然後修複我們的測試並將Mongoid安全升級到版本7。

把它們放在一起

總之,我們已經介紹了許多與支持異步工作負載的集成測試相關的主題。我們已經介紹了如何運行這些測試及其依賴關係、如何測試與外部服務依賴關係的作業交互,以及最後如何執行跨版本測試的基礎知識。請參閱下麵的docker-compose偽代碼,以了解這一切是如何組合在一起的。

最終的想法

隨著異步工作負載變得越來越普遍,了解如何執行有效的測試變得越來越重要。通過理解常見的故障模式以及構建可操作測試的方法,您可以將作業變得向後不兼容的可能性降到最低,並更有信心地迭代作業,減少對回歸的擔憂。

有興趣更多地了解Braze或獲得我們的技術和架構的第一手經驗嗎?查看我們的開放角色


馬克斯Gurewitz

相關內容

Braze如何融入團隊精神構建我們的信標設計體係

閱讀更多

雪花數據共享

Braze和Snowflake如何建立有效、持續的技術合作夥伴關係

閱讀更多

Braze如何通過協同合成促進團隊合作、洞察力和用戶體驗改進

閱讀更多

預測套件

測量釺焊預測套件中的預測精度

閱讀更多