由於數據應用開發和功能性軟件系統開發存在很大的不同,在我們實踐過程中,在開發人員和質量保證人員間常常有大量關於測試如何實施的討論。下文將嘗試總結一下數據應用開發的特點,並討論在這些特點之下,對應的測試策略應該是怎麼樣的。
功能性軟件的測試
先來回顧一下功能性軟件系統開發中的測試。
測試一般分爲自動化測試和手工測試。由於手工測試對人工依賴程度很高,如果主要依賴手工測試來保證軟件質量,將無法滿足軟件快速迭代上線的需要。現代軟件開發越來越強調自動化測試的作用,這也是敏捷軟件開發的基本要求。有了全方位的自動化測試保障,就有可能做到每週上線,每日上線甚至隨時上線。
這裏主要討論自動化測試。
測試金字塔
我們一般會按照如下測試金字塔的原則來組織自動化測試。
測試金字塔分爲三層,自下而上分別對應單元測試、集成測試、端到端測試。
單元測試是指函數或類級別的,較小範圍代碼的測試,一般不依賴外部系統(可通過Mock
或測試替身等實現)。單元測試的特點是運行速度非常快(最好全部在內存中運行),所以執行這種測試的成本也就很低。單元測試在測試金字塔的最底端,佔的面積最大。這指導我們應該構建大量的這類測試,並以這類測試爲主來保證軟件質量。
集成測試是比單元測試集成程度更高的測試,它在運行時執行的代碼路徑更廣,通常會依賴數據庫、文件系統等外部環境。由於依賴了外部環境,集成測試的運行速度更慢,執行測試的成本更高。集成測試在測試金字塔的中間,這指導我們應該構建中等數量的這類測試。集成測試在Web
應用場景中也常常被稱爲服務測試(Service Test)或API
測試。
端到端測試是比集成測試更靠後的測試,通常通過直接模擬用戶操作來構建這樣的測試。由於需要模擬用戶操作,所以它常常需要依賴一整套完整集成好的環境,這樣一來,其運行速度也是最慢的。端到端測試在Web
應用場景中也常常被稱爲UI
測試。端到端測試在測試金字塔的頂端,這指導我們應該構建少量的這類測試。
測試的範圍非常廣,實施方法也非常靈活。哪裏是重點?我們要在哪裏發力?測試金字塔爲我們指明瞭方向。
進入測試金字塔
爲了更深入地理解一般軟件的測試要怎麼做,我們需要進一步深入分析一下測試金字塔。
測試帶來的信心
上文中的金字塔圖示有一個特點並沒有反映出來,那就是,越上層的測試給團隊帶來的信心越強。這還算好理解,試想,如果沒有單元測試,只有端到端測試,我們是不是可以認爲程序大部分還是可以正常工作的(可能存在一些邊界場景有問題)?但是如果只有單元測試而沒有端到端測試,我們連程序能不能運行都不知道!
端到端測試能帶來很強的信心,但這常常構成另一個陷阱。由於端到端測試對團隊有很大的吸引力,一些團隊可能會選擇直接構建大量的端到端測試而忽略單元測試。這些端到端測試運行緩慢,一般也難以修改,很快就會讓團隊舉步維艱。緩慢的測試帶來了緩慢的持續集成,高頻率的上線就慢慢變得遙不可及。
單元測試雖然不能直接給人很強的信心,但是常常是更有效的測試手段,因爲它可以很容易的覆蓋到各種邊界場景。
測試金字塔是敏捷軟件開發所推崇的測試原則,它是在測試帶來的信心和測試本身的可維護性兩者中權衡做出的選擇。測試金字塔可以指導我們構建足夠的測試,使得團隊既對軟件質量有足夠的信心,又不會有太多的測試維護負擔。
既然是權衡,那麼我們是否可以以單元測試和集成測試爲主,而根本不構建端到端測試(此時端到端測試的功能通過手工測試完成)呢?
測試集成度
對於一些沒有UI
(或者說GUI
)的應用,或者一些程序庫、框架(如Spring
)等,很多時候測試金字塔中的三類測試並不直接適用。我們可以這樣理解:測試金字塔並非只是三層,它更多的是幫我們建立了在項目中組織測試的原則。
事實上,對於通用的軟件測試,我們可以理解爲存在一個集成度的屬性。沿着金字塔往上,測試的集成度越高(依賴外部組件越多)。由於集成度更高,測試過程所要運行的代碼就更多更復雜,測試運行時間就越長,測試構建和維護成本就越高。實踐過程中,爲了提高軟件質量和可維護性,我們應當構建更多集成度低的測試。
有了測試集成度的理解,我們就可以知道,其實金字塔可以不是三層,它完全可以是兩層或者四層、五層。這取決於我們怎麼劃定某一類測試的範圍。同時,我們還可以知道,其實單元測試、集成測試與端到端測試其實並沒有特別明顯的界限。
下面,我們從測試集成度的角度來看如何構建單元測試。
上文提到,測試最好通過Mock
或測試替身等實現,從而可以不依賴外部系統。但是,如果測試Mock
或測試替身難以構造,或者構造之後我們發現測試代碼和產品代碼耦合非常嚴重,這時應該怎麼辦呢?一個可能的選擇是考慮使用更高集成度的測試。
Spark
程序就是這樣的一個例子。一旦使用了Spark
的DataFrame
API
去編寫代碼,我們就幾乎無法通過Mock
Spark
的API
或構造一個Spark
測試替身的方式編寫測試。這時的測試就只能退一步選擇集成度更高一些的測試,比如,啓動一個本地的Spark
環境,然後在這個環境中運行測試。
此時,上面的測試屬於哪種測試呢?如果我們用三層測試金字塔的測試劃分來看待問題,就很難給這樣的測試一個準確的定位。不過,通常我們無需考慮這樣的分類,而是可以把它當做集成度低的測試,即金字塔靠底端的測試。如果團隊成員能達成一致,我們可以稱其爲單元測試,如果不能,稱其爲Spark
測試也並非不可。
何時停止測試
所以,對於一般的軟件測試,我們可以認爲測試策略應當符合一般意義的金字塔。金字塔的細節,比如應該有幾層塔,每一層的範圍應該是什麼樣,每一層應該用什麼樣的測試技術等等,這些問題需要根據具體的情況進行抉擇。
在討論一般軟件的測試時,需要關注軟件的測試何時停止,即,如何判斷軟件測試已經足夠了呢?
在老馬的《重構 第二版》中,有對於何時停止測試的觀點:
有一些測試規則建議會嘗試保證我們測試一切的組合,雖然這些建議值得了解,但是實踐中我們需要適可而止,因爲測試達到一定程度之後,其邊際效用會遞減。如果編寫太多測試,我們可能因爲工作量太大而氣餒。我們應該把注意力集中在最容易出錯的地方,最沒有信心的地方。
一些測試的指標,如覆蓋率,能一定程度上衡量測試是否全面而有效,但是最佳的衡量方式可能來自於主觀的感受,如果我們覺得對代碼比較有信心,那就說明我們的測試做的不錯了。
主觀的信心指數可能是衡量測試是否足夠的重要參考。如果要問測試是否足夠,我們要自問是否有信心軟件能正常工作。
在實踐過程中,我們還可以嘗試分析每次bug
出現的原因,如果是由於大部分bug
是由於代碼沒有測試覆蓋而產生的,此時我們可能應該編寫更多的測試。但如果是由於其他的原因,比如需求分析不足或場景設計不完備而導致的,則應該在對應的階段做加強,而不是一味的去添加測試。
數據應用的測試
有了前面對測試策略的分析,我們來看看數據應用的測試策略。
數據應用相比功能性軟件有很大的不同,但數據應用也屬於一般意義上的軟件。數據應用有哪些特點,應該如何針對性的做測試呢?下面我們來探討一下這幾個問題。
根據前面的文章分析,數據應用中的代碼可以大致分爲四類:基礎框架(如增強SQL
執行器)、以SQL
爲主的ETL
腳本、SQL
自定義函數(udf
)、數據工具(如前文提到的DWD
建模工具)。
基礎框架的測試
基礎框架代碼是數據應用的核心代碼,它不僅邏輯較爲複雜,而且需要在生產運行時支持大量的ETL
的運行。誰也不想提交了有問題的基礎框架代碼而導致大規模的ETL
運行失敗。所以我們應當非常重視基礎框架的測試,以保證這部分代碼的高質量。
基礎框架的代碼通常由Python
或Scala
編寫,由於Python
和Scala
語言本身都有很好的測試支持,這十分有利於我們做測試。
基礎框架的另一個特點是它通常沒有GUI
。
按照測試金字塔原理,我們應當爲其建立更多的集成度低的測試(下文稱單元測試)以及少量的集成度高的測試(下文稱集成測試)。
比如,在前面的文章中,我們增強了SQL
的語法,加入了變量、函數、模板等新的語法元素。在運行時進行變量替換、函數調用等等功能通過基礎框架實現。這部分功能邏輯較爲複雜,應當建立更多的單元測試及少量的集成測試。
ETL
腳本的測試
ETL
腳本的測試可能是數據應用中的最大難點。
採用偏集成的測試
ETL
腳本一般基於SQL
實現。SQL
本身是一個高度定製化的DSL
,如同XML
配置一樣。
XML
要如何測試?很多團隊可能會直接忽略這類測試。但是用SQL
編寫的ETL
代碼有時候還是可以達到幾百行的規模,有較多的邏輯,不測試的話難以給人以信心。如何測試呢?
如果採用基於Mock
的方法寫測試,我們會發現測試代碼跟產品代碼是一樣的。所以,這樣做意義不大。
如果採用高集成度的測試方式(下文稱集成測試),即運行ETL
並比對結果,我們將發現測試的編寫和維護成本都較高。由於ETL
腳本代碼本身可能是比較簡單且不易出錯的,爲了不易出錯的代碼編寫測試本身就必要性不高,更何況測試的編寫和維護成本還比較高。這就顯得集成測試這一做法事倍功半。
這裏可以舉一個例子。比如對於一個分組求和並排序輸出的SQL
,它的代碼可能是下圖這樣的。
如果我們去準備輸入數據和輸出數據,考慮到各種數據的組合場景,我們可能會花費很多的時間,這帶來了較高的測試編寫成本。並且,當我們要修改
SQL
時,我們還不得不修改測試,這帶來了維護成本。當我們要運行這個測試時,我們不得不完成建表、寫數據、運行腳本、比對結果的整個過程。這些過程都需要依賴外部系統,從而導致測試運行緩慢。這也是高維護成本的體現。
可見這兩種測試方式都不是好的測試方式。
測試構建原則
那麼有沒有什麼好的原則呢?我們從實踐中總結出了幾點比較有價值的思路供大家參考。
- 將
ETL
腳本分爲簡單ETL
和複雜ETL
(可以通過代碼行數,數據篩選條件多少等進行衡量)。簡單的ETL
通過代碼評審或結對編程來保證代碼質量,不做自動化測試。複雜的ETL
通過建立集成測試來保證質量。 - 由於集成測試運行較慢,可以考慮:
- 儘量少點用例數量,將多個用例合併爲一個來運行(主要是將數據可以合併成單一的一套數據來運行)
- 將測試分級爲需要頻繁運行的測試和無需頻繁運行的測試,比如可將測試分級P0-P5,P3-P5是經常(如每天或每次代碼提交)要運行的測試,P0-P2可以低頻(如每週)運行
- 開發測試支持工具,使得運行時可以儘量脫離緩慢的集羣環境。如使用
Spark
讀寫本地表
- 考慮將複雜的邏輯使用自定義函數實現,降低
ETL
腳本的複雜度。對自定義函數建立完整的單元測試。 - 將複雜的
ETL
腳本拆分爲多個簡單的ETL
腳本實現,從而降低單個ETL
腳本的複雜度。
加深對業務和數據的理解
我們在實踐過程中發現,其實大多數時候ETL
腳本的問題不在於代碼寫錯了,而在於對業務和數據理解不夠。比如,前面文章中的空調銷售的例子,如果我們在統計銷量的時候不知道存在退貨或者他店調貨的業務實際情況,那我們就不知道數據中還有一些字段能反映這個業務,也就不能正確的計算銷量了。
想要形成對數據的深入理解需要對長時間的業務知識積累和長時間對數據的探索分析(業務系統通常經歷了長時間的發展,在此期間內業務規則複雜性不斷增加,導致數據的複雜性不斷增加)。對於剛加入團隊的新人,他們更容易由於沒有考慮到某些業務情況而導致數據計算錯誤。
加深對業務和數據的理解是進行高效和高質量ETL
腳本開發的必由之路。
有沒有什麼好的實踐方法可以幫助我們加深理解呢?以下幾點是我們在實踐中總結的值得參考的建議:
- 通過思維導圖/流程圖來整理複雜的業務流程(或業務知識),形成知識庫
- 儘量多的進行數據探索,發掘容易忽略的領域業務知識,並通過第一步進行記錄
- 找業務系統團隊溝通,找出更多的領域業務知識,並通過第一步進行記錄
- 如果有條件,可以更頻繁的實地使用業務系統,總結更多的領域業務知識,並通過第一步進行記錄
- 針對第一步蒐集到的這些容易忽略的特定領域業務流程,設計自動化測試用例進行覆蓋
SQL自定義函數的測試
在基於Hadoop
的分佈式數據平臺環境下,SQL
自定義函數通常通過Python
或Scala
編寫。由於這些代碼通常對外部的依賴很少,通常只是單純的根據輸入數據計算得到輸出數據,所以對這些代碼建立測試是十分容易的事。事實上,我們很容易實現100%的測試覆蓋率。
在組織測試時,我們可以用單元測試的方式,不依賴計算框架。比如,以下Scala
編寫的自定義函數:
對其建立測試時,可以直接測試內部的轉換函數array_join_f
,一些示例的測試場景比如:
在建立了單元測試之後,一般還需要考慮建立少量的集成測試,即通過Spark
框架運行SQL
來測試此自定義函數,一個示例可以是:
如果自定義函數本身十分簡單,我們也可以直接通過Spark
測試來覆蓋所有場景。
從上面的討論可以看出,SQL
自定義函數是很容易測試的。除了好測試之外,SQL
自定義函數還有很多好的特性,比如可以很好的降低ETL
複雜度,可以很方便的被複用等。所以,我們應該儘量考慮將複雜的業務邏輯通過自定義函數封裝起來。這也是業界數據開發所建議的做法(大多數的數據開發框架都對自定義函數提供了很好的支持,如Hive
Presto
ClickHouse
等,大多數ETL
開發工具也都支持自定義函數的開發)。
數據工具的測試
數據工具的實例可以參考文章《數據倉庫建模自動化》和《數據開發支持工具》。
這些工具的一大特點是,它們是用於支持ETL
開發的,僅在開發過程中使用。由於它們並不是在產品環境中運行的代碼,所以我們可以降低對其的質量要求。
這些工具通常只是開發人員爲了提高開發效率而編寫的代碼,存在較大的修改和重構的可能,所以,過早的去建立較完善的測試必要性不高。
在我們的實踐過程中,這類代碼通常只有很少的測試,我們只對那些特別複雜、沒有信心能正確工作的地方建立單元測試。如果這些工具代碼是通過TDD
的方式編寫的,通常其測試會更多一些。
在持續集成流水線中運行測試
前面我們討論瞭如何針對數據應用編寫測試,還有一個關於測試的重要話題,那就是如何在持續交付流水線中運行這些測試。
在功能性軟件項目中,如果我們按照測試金字塔的三層來組織測試,那麼在流水線中一般就會對應三個測試過程。
從上面的討論可知,數據應用的測試被縱向分爲四條線,如何對應到流水線上呢?如果我們採用同一個代碼庫管理所有的代碼,可以考慮直接將流水線分爲四條並行的流程,分別對應這四條線。如果是不同的代碼庫,則可以考慮對不同的代碼庫建立不同的流水線。在每條流水線內部,就可以按照單元測試、低集成測試、高集成測試這樣的方式組織流水線任務。
一、獨立的ETL
流水線
對於ETL
代碼的測試,有一個值得思考的問題。那就是,ETL
腳本之間通常獨立性非常強,相互之間沒有依賴。這是由於ETL
代碼常常由完善的領域特定語言SQL
開發而成,與Python
或Scala
等通用編程語言編寫的代碼不同,SQL
文件之間是沒有依賴的(如果說有依賴,那也是通過數據庫表產生依賴)。
既然如此,假設我們修改了某一個ETL
文件的代碼,是不是我們可以不用運行其他的ETL
文件的測試呢?其實不僅如此,我們甚至可以單獨上線部署此ETL
,而不是一次性部署所有的ETL
。這在一定程度上還降低了部署代碼帶來的風險。
有了上面的發現,我們可能要重新思考數據應用的持續交付流水線組織形式。
一個可能的辦法是爲每一個ETL
文件建立一個流水線,完成測試、部署的任務。此時每個ETL
可以理解爲一個獨立的小程序。
這樣的想法在實踐中不容易落地,因爲這將導致大量的流水線存在(常常有上百條),從而給流水線工具帶來了很大的壓力。常用的流水線工具,如Jenkins
,其設計是難以支撐這麼大規模的流水線的創建和管理的。
要如何來支持上面這樣的ETL
流水線呢?可能需要我們開發額外的流水線工具纔行。
二、雲服務中的ETL
流水線
現在的一些雲服務廠商在嘗試這樣做。他們通常會提供一個基於Web
的ETL
開發工具,同時會提供工具對當前的ETL
的編寫測試。此時,ETL
開發人員可以在一個地方完成開發、測試、上線,這可以提高開發效率。
這類服務的一個常見缺點在於它嘗試用一套Web
系統來支持所有的ETL
開發過程,這帶來了大量繁雜的配置。這其實是將ETL
開發過程的複雜性轉化爲了配置的複雜性。相比編寫代碼而言,多數開發人員不會喜歡這樣的工作方式。(當前軟件開發所推崇的是Everthing as Code
的做法,嘗試將所有開發相關過程中的東西代碼化,從而可以更好的利用成熟的代碼編輯器、版本管理等功能。而Web
配置的方式與Everthing as Code
背道而馳。)
對於這些數據雲服務廠商提供的數據開發服務,如果可以同時支持通過代碼和Web
界面配置來實現數據開發,那將能得到更多開發者的喜愛。這在我看來是一個不錯的發展方向。
總結
由於數據應用開發有很強的獨特的特點(比如以SQL
爲主、有較多的支撐工具等),其測試與功能性軟件開發的測試也存在很大的不同。
本文分析瞭如何在測試金字塔的指導下制定測試策略。測試金字塔不僅可以很好的指導功能性軟件開發,在進行一般意義上的推廣之後,可以很容易得到一般軟件的測試策略。關於測試金字塔,本文分析了測試帶來的質量信心及測試集成度,這兩個概念可以幫助我們更深刻的理解測試金字塔背後的指導原則。
在最後,結合我們的實踐經驗,給出了一些數據應用中的測試構建實踐。將數據應用分爲四個不同模塊來分別構建測試,可以很好的應對數據應用中的質量要求,同時保證有較好的可維護性。最後,我們討論瞭如何在持續集成流水線中設計測試任務,留下了一個有待探索的方向,即如何針對單個ETL
構建流水線。
數據應用的質量保證是不容易做到的,常常需要我們進行很多的權衡取捨才能找到最適合的方式。想要解決這一問題,還要發揮團隊中所有人的能動性,多總結和思考纔行。
文/Thoughtworks 廖光明
原文鏈接:https://insights.thoughtworks.cn/testing-pyramid-guide-data-application-test/