單元測試被高估了 - tyrrrz

banq發表於2022-02-22

測試在現代軟體開發中的重要性怎麼強調都不為過。交付一個成功的產品不是你做一次就忘記的事情,而是一個不斷重複的過程。隨著每一行程式碼的更改,軟體必須保持功能狀態,這意味著需要進行嚴格的測試。
隨著時間的推移,隨著軟體行業的發展,測試實踐也日趨成熟。逐漸走向自動化,測試方法影響了軟體設計本身,催生了諸如測試驅動開發之類的口頭禪,強調了依賴倒置等模式,並普及了圍繞它構建的高階架構
如今,自動化測試已深深嵌入我們對軟體開發的認知中,很難想象其中一個沒有另一個。而且由於這最終使我們能夠在不犧牲質量的情況下快速生產軟體,因此很難說這不是一件好事。
然而,儘管有許多不同的方法,現代“最佳實踐”主要推動開發人員專門進行單元測試。
這種方法的好處通常得到以下論點的支援:單元測試在開發過程中提供最大價值,因為它們能夠快速捕獲錯誤並幫助實施促進模組化的設計模式。這個想法已被廣泛接受,以至於“單元測試”一詞現在在某種程度上與一般的自動化測試混為一談,失去了部分含義並導致混淆。
當我還是一個經驗不足的開發人員時,我相信遵循這些“最佳實踐”,因為我認為這會使我的程式碼變得更好。由於涉及抽象和模擬的所有儀式,我並不是特別喜歡編寫單元測試,但畢竟這是推薦的方法,所以我應該更瞭解誰。
直到後來,隨著我進行了更多的實驗並構建了更多的專案,我才開始意識到有更好的方法來進行測試,而專注於單元測試在大多數情況下完全是浪費時間。
 

積極普及的“最佳實踐”通常傾向於在他們周圍表現出貨物崇拜,誘使開發人員應用設計模式或使用特定方法,而不給他們急需的重新考慮。在自動化測試的背景下,當談到我們行業對單元測試的不健康痴迷時,我發現這很普遍。
在本文中,我將分享我對這種測試技術的觀察,並討論為什麼我認為它效率低下。我還將解釋我目前在開源專案和日常工作中使用哪些方法來測試我的程式碼。
  

單元測試的謬誤
單元測試,顧名思義,圍繞“單元”的概念展開,它表示較大系統中非常小的孤立部分。對於一個單元是什麼或它應該有多小沒有正式的定義,但大多數人認為它對應於模組的單個功能(或物件的方法)。
通常,當編寫程式碼時沒有考慮到單元測試,可能無法完全隔離地測試某些功能,因為它們可能具有外部依賴關係。為了解決這個問題,我們可以應用依賴倒置原則,用抽象替換具體的依賴。然後,這些抽象可以用真實或虛假的實現代替,具體取決於程式碼是正常執行還是作為測試的一部分。
除此之外,單元測試應該是純粹的。例如,如果一個函式包含將資料寫入檔案系統的程式碼,則該部分也需要抽象出來,否則驗證這種行為的測試將被視為整合測試,因為它的覆蓋範圍擴充套件到單元與檔案系統。
考慮到上述因素,我們可以推斷單元測試僅對驗證給定函式內部的純業務邏輯有用。它們的範圍沒有擴充套件到測試副作用或其他整合,因為這屬於整合測試領域。

  • 單元測試的目的有限

重要的是要理解任何單元測試的目的都非常簡單:在隔離範圍內驗證業務邏輯。根據您打算測試的互動,單元測試可能是也可能不是適合這項工作的工具。
例如,對使用漫長而複雜的數學演算法計算太陽時的方法進行單元測試是否有意義?很可能,是的。
對向 REST API 傳送請求以獲取地理座標的方法進行單元測試是否有意義?很可能,不是。
如果您將單元測試本身視為一個目標,您很快就會發現,儘管付出了很多努力,但大多數測試仍無法為您提供所需的信心,這僅僅是因為它們測試了錯誤的東西。在許多情況下,測試與整合測試更廣泛的互動比專門關注單元測試更有益。
有趣的是,一些開發人員最終確實在這種情況下編寫了整合測試,但仍然將它們稱為單元測試,主要是由於圍繞概念的混淆。儘管可以爭辯說可以任意選擇單元大小並且可以跨越多個元件,但這使得定義非常模糊,最終只是使該術語的整體使用完全無用。
 
  • 單元測試導致更復雜的設計

支援單元測試的最流行的論點之一是它強制您以高度模組化的方式設計軟體。這是建立在這樣一個假設之上的,即當程式碼被分成許多較小的元件而不是幾個較大的元件時,它更容易推理程式碼。
但是,它通常會導致相反的問題,即功能最終可能會變得不必要地分散。這使得評估程式碼變得更加困難,因為開發人員需要掃描構成應該是單個內聚元素的多個元件。
此外,實現元件隔離所需的抽象的大量使用建立了許多不需要的間接。儘管抽象本身是一種非常強大和有用的技術,但抽象不可避免地會增加認知複雜性,從而使推理程式碼變得更加困難。
透過這種間接方式,我們最終也會失去某種程度的封裝,否則我們可以保持這種封裝。例如,管理單個依賴項生命週期的責任從包含它們的元件轉移到其他一些不相關的服務(通常是依賴項容器)。
一些基礎設施複雜性也可以委託給依賴注入框架,從而更容易配置、管理和啟用依賴項。但是,這會降低可移植性,這在某些情況下可能是不可取的,例如在編寫庫時。
歸根結底,雖然很明顯單元測試確實會影響軟體設計,但這是否真的是一件好事還存在很大爭議。
 
  • 單元測試很昂貴

從邏輯上講,假設由於它們很小且孤立,單元測試應該非常容易和快速地編寫是有道理的。不幸的是,這只是另一個似乎相當流行的謬論,尤其是在經理中。
儘管前面提到的模組化架構誘使我們認為各個元件可以彼此分開考慮,但單元測試實際上並沒有從中受益。事實上,單元測試的複雜性僅與單元具有的外部互動的數量成正比增長,因為您必須做所有工作來實現隔離,同時仍然執行所需的行為。
本文前面展示的示例非常簡單,但在實際專案中,安排階段跨越許多長行並不罕見,只是為單個測試設定前提條件。在某些情況下,被模擬的行為可能非常複雜,幾乎不可能將其解開以弄清楚它應該做什麼。
除此之外,單元測試在設計上與他們正在測試的程式碼非常緊密地耦合,這意味著任何進行更改的努力都會有效地加倍,因為測試套件也需要更新。更糟糕的是,很少有開發人員似乎覺得這樣做是一項誘人的任務,通常只是把它交給團隊中更年輕的成員。
  
  • 單元測試依賴於實現細節

基於模擬的單元測試的不幸含義是,用這種方法編寫的任何測試都是固有的實現意識。透過模擬一個特定的依賴關係,你的測試變得依賴於被測試的程式碼如何消耗這個依賴關係,而這個依賴關係並沒有被公共介面所規範。

這種額外的耦合往往會導致意想不到的問題,在這種情況下,看似非破壞性的變化會導致測試開始失敗,因為模擬變得過時了。這可能是非常令人沮喪的,並最終使開發人員不願意嘗試重構程式碼,因為永遠不清楚測試中的錯誤是來自於實際的迴歸還是由於依賴某些實現細節。

單元測試有狀態的程式碼可能更加棘手,因為可能無法透過公開暴露的介面來觀察變異。為了解決這個問題,你通常會注入間諜,這是一種模擬的行為,記錄函式被呼叫的時間,幫助你確保單元正確使用它的依賴關係。

當然,當你不僅依賴一個特定的函式被呼叫,而且還依賴它發生了多少次或傳遞了哪些引數時,測試就變得與實現更加耦合了。以這種方式編寫的測試,只有在內部細節不發生變化的情況下才有用,而這是一個非常不合理的期望。

過分依賴實現細節也會使測試本身變得非常複雜,考慮到為了模擬一個特定的行為需要進行多少設定,特別是當互動不是那麼微不足道或者有很多依賴關係時。當測試變得如此複雜,以至於他們自己的行為難以推理時,誰會去寫測試來測試?

 
  • 單元測試不能測試使用者行為

無論你開發的是什麼型別的軟體,其目標都是為終端使用者提供價值。事實上,我們寫自動化測試的主要原因是為了確保沒有意外的缺陷會削弱這種價值。

在大多數情況下,使用者透過一些頂層介面(如使用者介面、CLI或API)來使用軟體。雖然程式碼本身可能涉及許多抽象層,但對使用者來說,唯一重要的是他們能實際看到並與之互動的那一層。

如果在系統的某些部分有一個錯誤,這甚至並不重要,只要它沒有浮現在使用者面前,並且不影響提供的功能。反過來說,如果在使用者介面上有一個缺陷,使我們的系統實際上毫無用處,那麼我們可能對所有的底層部分都有全面的覆蓋,這也沒有什麼區別。

當然,如果你想確保某樣東西能正常工作,你就必須檢查那件確切的東西,看看它是否能做到。在我們的例子中,獲得對系統信心的最好方法是模擬一個真實的使用者如何與頂層介面互動,看看它是否按照預期正常工作。

單元測試的問題是,它們與此完全相反。因為我們總是在處理使用者不直接互動的孤立的小塊程式碼,所以我們從未測試過實際的使用者行為。

做基於模擬的測試使這種測試的價值受到了更大的質疑,因為我們系統中本來要使用的部分被替換成了模擬,進一步拉開了模擬環境與現實的距離。我們不可能透過測試與使用者體驗不相似的東西來獲得使用者會有一個順利體驗的信心。

 

現實驅動的測試

最好編寫儘可能高度整合的測試,同時保持它們的速度和複雜性合理。
根據行為線索而不是程式碼的內部結構來劃分測試是一個好主意。
 

  • 純度與非純度分離

基於純度的程式碼分離的基本原則非常重要,但經常被忽視。如果使用得當,它可以指導軟體設計,在可讀性、可移植性和單元測試方面帶來好處。
純度是一個非常有用的概念,因為它可以幫助我們理解某些操作如何使我們的程式碼具有不確定性、難以推理以及單獨測試很麻煩。不純的互動本身並不壞,但它們施加的約束本質上具有傳染性,並可能傳播到應用程式的其他部分。
純不純分離原則旨在透過將雜質與程式碼的其餘部分分離,將它們限制在最低限度。最終,目標是將所有非純操作推向系統的最外層,同時保持領域層完全由純函式組成。
以這種方式設計軟體會導致類似於管道而不是層次結構的體系結構,這有利於程式設計的功能風格。根據專案的不同,這可能有助於更清楚地表達資料流以及其他有用的好處。
但是,這並不總是可行的,並且在某些情況下,提取純程式碼會以嚴重降低內聚性為代價。

 

概括
單元測試是一種流行的軟體測試方法,但主要是出於錯誤的原因。它通常被吹捧為開發人員測試程式碼同時執行最佳設計實踐的一種有效方式,但許多人認為它既累贅又膚淺。
重要的是要了解開發測試並不等同於單元測試。主要目標不是編寫儘可能獨立的測試,而是獲得對程式碼根據其功能要求工作的信心。並且有更好的方法來實現這一目標。
從長遠來看,編寫由使用者行為驅動的高階測試將為您提供更高的投資回報,而且並不像看起來那麼難。找到一種對您的專案最有意義的方法並堅持下去。
以下是主要內容:

  1. 批判性思考並挑戰最佳實踐
  2. 不要依賴測試金字塔
  3. 按功能分離測試,而不是按類、模組或範圍
  4. 以最高水平的整合為目標,同時保持合理的速度和成本
  5. 避免為了可測試性而犧牲軟體設計


單元測試被高估了  - tyrrrz

相關文章