關於單元測試體系結構的一些心得

自動化測試是任何大型軟件項目不可或缺的一部分,可作為提高質量,生產率和靈活性的一種手段。因此,至關重要的是,系統架構的設計必須能夠促進自動化測試的開發和執行。

質量得到提高,因為自動化測試的執行可以讓我們找到,並在開發週期的早期解決問題,很多之前產品變更部署到生產和可用給最終用戶。

生產率提高是因為在開發週期中發現問題的時間越早,修復該問題的成本越低,並且不難理解為什麼。如果軟件開發人員能夠在將代碼更改集成到主存儲庫之前運行自動化測試套件,則他可以快速發現新引入的錯誤並將其修復。但是,如果沒有這樣的測試套件,則新引入的錯誤可能只會在以後由最終用戶報告的手動測試階段中出現,甚至更糟,這要求開發人員退出常規開發工作流程以進行調查和修復。

靈活性得到了改善,因為開發人員在依賴於測試覆蓋率較高的測試套件來評估其代碼更改的影響時,對重構代碼,升級程序包以及在需要時修改系統行為更有信心。

在討論自動化測試時,我也喜歡將風險管理的話題引入對話中。作為首席軟件工程師,風險管理是我工作的重要組成部分,它涉及對開發團隊進行實踐和流程指導,以減少產品技術退化的風險。從上面列出的好處中可以明顯看出,採用適當的自動化測試策略很合適,可以幫助減輕軟件項目中的風險。

展望未來,我們可以根據實現和運行自動測試的策略將自動測試分為至少三種不同類型,如以下著名的測試金字塔所示:

關於單元測試體系結構的一些心得

就使用的時間和資源而言,單元測試的開發成本低且運行成本低,並且單元測試專注於測試與外部依賴項隔離的單個系統組件(例如,業務邏輯)。

集成測試向前邁了一步,並且在不隔離外部依賴關係的情況下進行了開發和運行。在這種情況下,我們有興趣評估所有系統組件在組合在一起並面臨集成約束(例如:聯網,存儲,處理等)時是否按預期進行交互。

最後,在金字塔的頂端,圖形用戶界面測試是自動化和執行最昂貴的。他們通常依靠UI輸入/輸出腳本和回放工具來模仿最終用戶與系統圖形用戶界面的交互。

在本文中,我們將重點介紹測試金字塔的基礎,單元測試以及促進採用它們的系統體系結構注意事項。

有效單元測試的屬性

讓我們列舉一下什麼是有效的,精心設計的單元測試。以下是一個命題:

簡短,只有一個目的

簡單,清晰的設置和拆卸

快速,只需幾分之一秒即可執行

標準化,遵循嚴格的約定

理想情況下,單元測試應顯示所有這些屬性,下面我詳細說明原因。

如果單元測試不夠短,將很難閱讀並理解其目的,即確切地說是測試的內容。因此,出於這個原因,單元測試應該有一個明確的目標,並且只評估一件事,而不是嘗試同時執行多個評估。這樣,當單元測試失敗時,開發人員將更加輕鬆快捷地評估情況並進行修復。

如果單元測試需要大量的精力來設置他們的測試環境,然後將其拆除,則開發人員通常會開始質疑,花費在編寫這些測試上的時間是否值得。因此,我們需要提供一個編寫單元測試的環境,該環境要管理測試上下文的所有複雜性,例如注入依賴關係,預加載數據,清除緩存等。編寫單元測試越容易,開發人員創建它們的動力就越大!

如果執行一組單元測試要花費大量時間,則開發人員自然會減少執行頻率。這裡的危險在於擁有如此冗長的單元測試套件,以至於變得不切實際,開發人員開始跳過運行它或有選擇地運行它,從而降低了其有效性。

最後,如果測試未標準化,不久後您的測試套件將開始看起來像狂野的西部,編寫單元測試所使用的編碼風格有時會有所不同,有時會發生衝突。因此,在整個單元測試範圍內,追求系統設計的一致性對於整個系統來說同樣有效。

一旦我們對有效的單元測試的構成達成共識,就可以開始定義提升其性能的系統架構準則,如以下各節所述。

軟件複雜度

除其他因素外,軟件複雜性還源於系統中組件之間不斷增長的交互次數以及內部狀態的演變。隨著複雜度的提高,無意識地干擾複雜的組件交互網絡的風險也隨之增加,有可能導致在更改代碼時引入缺陷。

此外,根據常識,系統的複雜性越高,維護和測試它就越困難,這導致了第一個(一般)準則:

密切關注軟件的複雜性並遵循設計實踐來控制它

在管理複雜性同時提高可測試性時,值得一提的做法是在系統設計中儘可能採用“ 純函數”和“ 不變性”。純函數是具有以下屬性的函數:1

對於相同的參數,其返回值是相同的(局部靜態變量,非局部變量,可變參考變量或來自I / O設備的輸入流無變化)。

它的評估沒有副作用(本地靜態變量,非本地變量,可變引用參數或I / O流不會發生突變)。

從其特性可以明顯看出,純函數非常適合於單元測試。它們的用法也消除了對許多補充性實踐的需求,這些補充性實踐將在以下各節中討論,以處理大多數有狀態的組件。

不變性同樣重要。不可變對象是創建後狀態無法更改的對象。它們更易於交互且更可預測,從而有助於降低系統複雜性,消除全局狀態。

隔離依賴

按照它們的定義,單元測試旨在隔離地測試各個系統組件,因為我們不希望組件的單元測試的結果受到其依賴項之一的影響。隔離程度會根據被測組件的具體情況以及每個開發團隊的偏好而有所不同。我個人不擔心隔離輕量級的內部業務類,因為我發現用測試目標組件替代它們並沒有增加任何價值,該組件將顯示幾乎相同的行為。儘管如此,這裡的策略可能很簡單:

在組件設計中應用依賴項反轉模式

依賴關係反轉模式(DIP)指出,高級對象和低級對象都應依賴抽象(例如接口),而不是特定的具體實現。一旦將系統組件從其依賴關係中分離出來,我們就可以在單元測試的上下文中通過簡化的,針對測試的具體實現輕鬆地替換它們。下面的類圖說明了結果結構:

關於單元測試體系結構的一些心得

在此示例中,被測組件依賴於資料庫和文件存儲抽象。當部署到生產環境中時,我們可能會為存儲庫類注入基於SQL的具體實現,併為文件存儲組件注入基於S3的實現,以便在AWS Cloud中遠程存儲文件。但是,在運行單元測試時,我們將希望注入不依賴外部服務的簡化功能實現,例如以綠色繪製的“內存中”實現。

如果您不熟悉DIP,那麼我還會發表另一篇文章,內容是有關如何在可能會有所幫助的相似上下文中使用DIP的實用概述:集成第三方模塊。

假裝與假貨辯論

請注意,我並不是將這些“內存中”實現稱為“模仿”,它們是模擬對象,它們以有限的受控方式模仿了真實對象的行為。我故意這樣做,因為我反對使用模擬對象,而建議使用完全兼容的“偽”實現,這為我們提供了編寫單元測試的更大靈活性,並且可以比設置更可靠的方式在多個單元測試類中重用模擬。

為了更詳細地說明,假設我們正在編寫一個依賴於組件的單元測試。 文件存儲抽象。在此測試中,組件將一個項目添加到文件存儲中,但實際上並不擔心操作是成功還是失敗(例如,日誌文件),因此,我們決定以“虛擬”方式模擬該操作。現在,假設稍後需求發生變化,並且組件需要確保在繼續操作之前通過從文件存儲中讀取文件來創建文件,這迫使我們更新模擬的行為以使測試通過。然後,想象需求再次發生變化,並且組件需要寫入多個文件(例如:每個日誌級別一個),而不是僅寫入一個,從而迫使我們的模擬對象行為得到另一種改善。你知道發生了什麼嗎?我們正在逐步改進我們的模擬,使其更類似於具體的實現。更糟糕的是,我們最終可能會遇到許多獨立的,半生不熟的,

為了解決這種情況,我提出以下準則:

依靠Fakes而不是Mocks來實施單元測試,將它們視為一流的公民,並將其組織為可重用的模塊

由於Fake組件實現了商業行為,因此與設置模擬相比,它們本質上是更昂貴的初始投資。但是,它們的長期回報肯定更高,並且更符合有效的單元測試的特性。

編碼風格

每個自動化測試都可以描述為三步腳本:

1、準備測試環境

2、執行按鍵操作

3、驗證結果


這是合乎邏輯的考慮,給出一個初始已知狀態,當執行一個操作,那麼就應該產生相同的預期結果,每一次。為了使結果變得不同,必須更改初始狀態,或者更改操作實現本身。

您可能對上面用黑體字標出的單詞很熟悉。如果不是這樣,它們代表了一種流行的Given-When-Then模式,以一種有利於可讀性和結構的方式編寫單元測試。這裡的想法很簡單:

為編寫單元測試定義並實施單一的標準化編碼樣式

可以使用多種方式採用“當時給定”模式。其中之一是將單元測試方法構造為三種不同的方法。例如,考慮密碼強度測試:

關於單元測試體系結構的一些心得

使用這種方法,主要的測試方法變成了對單元測試目的的三行描述,即使是非開發人員也可以通過閱讀它輕鬆地理解。在實踐中,單元測試的主要方法最終成為系統行為的低級文檔,不僅提供文本描述,還提供執行代碼,調試代碼並找出內部情況的可能性。當新開發人員加入團隊時,這對於縮短系統架構學習曲線非常有價值。

重要的是要強調,在編碼風格方面,沒有唯一正確的方法。我在上面提供的示例可能會使某些開發人員感到不滿,例如,因為冗長而令人不悅,這沒關係。真正重要的是,在您的開發團隊中就編碼慣例達成協議,以編寫對您有意義並堅持下去的單元測試。

管理測試環境

單元測試上下文管理是一個討論不夠多的主題。“測試上下文”是指成功運行單元測試所需的整個依賴項注入和初始狀態設置。

如前所述,當開發人員花費較少的時間來擔心設置測試上下文並花更多時間編寫測試用例時,單元測試會更有效。我們從以下觀察得出我們的最後一個準則,即大量測試案例可以共享一些測試上下文:

利用構建器類將測試上下文的構建與單元測試用例的實現分開

這個想法是將測試上下文的構造邏輯封裝在構建器類中,並在單元測試類中引用它們。然後,每個上下文構建器負責創建特定的測試方案,並可選地定義用於使其特定化的方法。

讓我們看一下另一個說明性的代碼示例。假設我們正在開發一個反欺詐組件,用於檢測移動應用程序用戶的可疑位置變化。測試上下文生成器可能如下所示:

關於單元測試體系結構的一些心得

關於單元測試體系結構的一些心得

由此創建的測試上下文MobileUserContextBuilder足夠通用,因此從應用程序已經註冊了移動用戶的狀態開始所需的任何測試用例都可以使用它。最重要的是,它定義了AddDevice具體化測試環境以適應我們虛擬的反欺詐組件測試需求的方法。

考慮調用此反欺詐組件GeolocationScreener,它負責檢查移動用戶的位置是否更改得太快,這表明他可能是在偽造自己的真實座標。其單元測試之一可能如下所示:

關於單元測試體系結構的一些心得

關於單元測試體系結構的一些心得

可見,在此示例測試類中專用於設置測試上下文的代碼量很小,因為它幾乎完全包含在builder類中,從而保留了代碼的可讀性和組織性。隨著越來越多的測試用例利用可用的測試上下文構建器庫,設置測試上下文所需的攤銷時間變得非常短。

結論

在本文中,我討論了單元測試的主題,提供了五個主要指南,以應對在不斷增長的測試用例中保持有效性的挑戰。這些準則對系統體系結構有重要影響,從軟件項目開始就應該考慮單元測試要求,以促進開發人員在其中看到價值並有動力編寫單元測試的環境。

單元測試應被視為系統體系結構的組成部分,與它們所測試的組件一樣重要,而不應被視為開發團隊僅出於填寫管理報告複選框或提供指標而編寫的二等公民。

最後,如果您正在使用很少或沒有單元測試的遺留項目中,而沒有使用DIP,則由於您有意避免談論複雜的模擬框架,因此本篇文章可能沒有為您提供最佳策略。遺留項目的上下文成為將單元測試引入極度耦合的代碼的可行選擇。



分享到:


相關文章: