單元測試從入門到精通
當前位置:點晴教程→知識管理交流
→『 技術文檔交流 』
1 前言
這篇文章源于工作中的一個項目,2021年,我負責匯川技術工業機器人應用軟件的基礎架構重構,當時單元測試是重構工作的核心環節之一,從無法進行單元測試到最終60%以上的行覆蓋率,過程中自己也有非常多的收獲,于是將其整理成文,希望對計劃開展和正在開展單元測試的同學有所幫助。 2 什么是單元測試
單元測試(Unit Testing),是指對軟件中的邏輯單元或組件進行檢查和驗證,以確保其按預期執行。通常單元測試是軟件開發過程中進行的最低級別測試活動,通過單元測試可發現和修復軟件開發早期的BUG和缺陷。 單元(Unit),是一個應用程序中最小的可測試部分,在面向過程開發中,單元通常為函數(Function),在面向對象開發中,單元通常為類中的方法(Method)。 3 為什么要進行單元測試
3.1 降低代碼缺陷
單元測試的首要目標是降低代碼缺陷,如上圖所示,當代碼缺陷越早被發現,它的修復成本就越低,這種把測試盡量提前進行的思想就叫做測試左移。 3.2 推動架構優化
單元測試與軟件架構有著非常緊密的聯系,通常越是架構設計優秀的項目(比如符合SOLID規則),越容易實施單元測試,反之越是架構糟糕的項目,越難以實施單元測試。并且單元測試以及其嚴格的方式要求軟件架構設計,如果架構設計存在問題,單元測試就根本無法開展。 假如你發現在自己的項目中實施單元測試舉步為艱,那么首先應該停下來觀察和思考一下,項目架構設計的是否合理?比如一些項目中UI與業務邏輯耦合,單元測試無法命中核心業務邏輯,可思考一下項目是否需要分層設計?UI與業務邏輯是否需要分離?比如項目中當碰到物理設備的依賴,單元測試就被阻斷,可思考一下是否永遠只使用這一臺設備?后續有沒有可能換成其它設備?是否需要考慮擴展性?再比如發現被測對象就是鐵板一塊,根本不能改變其協作對象的行為和數據,那么就應思考一下,對象之間是否存在強耦合?是否可以通過依賴注入降低和消除對象之間的耦合? 大多數情況我們在項目中實施單元測試的目的是為了保障代碼質量,但我認為單元測試對軟件架構優化的驅動實際更為重要。 3.3 守護代碼迭代質量
在當下的商業環境中,大魚吃小魚,快魚吃慢魚,對軟件開發的效率要求越來越高,傳統的瀑布開發模式越來越少,敏捷開發模式越來越普及。因此軟件版本快速迭代,快速測試,快速發布,小步快跑在大多數項目中成為常態。 而單元測試在代碼快速迭代過程中發揮著守護代碼質量的至關重要作用。當單元測試覆蓋了一個模塊中的業務邏輯,該業務邏輯在迭代變更過程中出現任何問題,會第一時間自動被單元測試捕獲,因此單元測試對發生變更的代碼正確性提供了保障,同時開發人員在這樣的保障下可以大膽的對代碼進行重構,對業務邏輯進行增減變更調整。大名鼎鼎的TDD(測試驅動開發)就是基于這個原理。 4 如何進行單元測試
4.1 使用AAA規則編寫測試用例
我們用一個簡單的示例來演示單元測試的編碼過程,如下代碼所示,是一個非常簡單的方法,它根據不同的距離,推薦不同的交通工具:
然后我們為這個方法編寫單元測試用例,它同樣非常的簡單:設定條件,調用被測方法,斷言返回結果:
至此,單元測試的編碼就完成了。是的,單元測試的編碼已全部完成了,花30秒看懂這個示例,你就掌握了單元測試的核心方法。為了方便記憶,有人將它總結成了AAA規則: Arrange
設置條件 Act
執行邏輯 Assert
斷言結果 4.2 讓每個測試用例符合AIR特性
AAA規則告訴我們如何編寫單元測試用例,但要編寫一個合格的單元測試用例,就需要了解單元測試用例的基本特性,這些特性就像空氣(AIR)一樣重要,任何時候我們也不能離開: Automatic
自動化:單元測試應自動執行,而無需任何交互,測試用例通常被定期執行。 Independent
獨立性:每個單元測試用例都是獨立的個體,不允許測試用例之間存在依賴關系,也不允許要求測試用例被執行的先后順序。 Repeatable
可重復:單元測試用例在被重復執行時應穩定的返回相同的結果,不能受外部環境的影響。 4.3 在需要的時候使用測試替身
什么是測試替身
比如業務中我們的代碼與硬件設備連接,需要依賴硬件的不同狀態來執行不同的邏輯。單元測試的特性是隨時可重復執行,對硬件的依賴會阻塞單元測試執行,因此在單元測試用例中需要用一個“替身”替換掉硬件的狀態,這個“替身”就叫做測試替身。 測試替身的應用場景
1、真實對象具有不可確定的行為,或產生不可預測的結果。 2、真實對象很難被創建或創建成本過大,比如第三方系統、與硬件設備關聯的模塊。 3、真實對象的某些行為很難觸發,比如異常的觸發。 4、真實對象令測試用例的執行速度很慢。 5、真實對象有含有人機交互界面。 4.4 了解單元測試覆蓋方式
語句覆蓋
語句覆蓋又稱為行覆蓋,是單元測試中最簡單也是最常見的覆蓋率統計方式。被測函數中,只要被單元測試用例執行到的行,即認為該行被覆蓋到。比如一個100行的函數,其中有60行被單元測試用例執行到,那么語句覆蓋率為60%。 分支覆蓋
分支覆蓋又稱為判定覆蓋,它關注的是被測函數中產生分支的if判定結果,只要每個if語句判定為真和判定為假的分支都被執行到,即達成了分支覆蓋。注意分支覆蓋并不考慮多個分支間的組合關系。 條件覆蓋
條件覆蓋關注的是判定語句中的每個表達式是否被執行。比如判定語句 if (a() || b()) ,當a()返回為真時就不再執行b()了,此時就未達成條件覆蓋;要達成條件覆蓋,就需要使a()返回假。 路徑覆蓋
路徑覆蓋是單元測試中覆蓋最全的一種方式,它要求覆蓋被測試方法中所有邏輯分支路徑的組合。 總結
語句覆蓋在單元測試覆蓋率統計中最為常見,基本是一個必選項,分支覆蓋與條件覆蓋可作為進階選擇,路徑覆蓋最為完善,但是在復雜的業務場景中,會導致單元測試代碼指數級增長。建議根據實際情況靈活組合搭配。 4.5 單元測試過程中的一些常見疑問
由誰來編寫單元測試用例?
應該由開發人員編寫自己開發的功能對應的單元測試用例。有些項目中會安排專人來為其它人開發的功能編寫單元測試用例,這樣做效率很低,因為單元測試用例的編寫人員需要花費時間了解和學習代碼邏輯。 什么時間節點寫單元測試用例?
通常應該在功能開發完成后即編寫與之對應的單元測試用例,即使有延遲也不要延遲太長時間,時間過長會導致編寫單元測試用例時需要重新回顧代碼邏輯所帶來的額外時間成本。 單元測試是白盒還是黑盒測試?
絕大部分的單元測試是白盒測試,會根據函數中的邏輯設計編寫測試用例,以達到覆蓋率目標。但單元測試也可以是黑盒測試,比如一些API接口只關注輸入與輸出而不關注內部的邏輯實現。 5 可測試性設計
5.1 分層設計
將一件復雜的事情進行分解,是提升效率的基本手段,這在日常生活中非常常見。比如汽車的生產過程離散在多個零部件生產線,最后完成組裝。軟件中的分層設計,也是最常見的一種架構模式,在流行的開發框架中隨處可見。分層設計可以幫助單元測試準確的命中目標,比如通常情況下我們并不需要對UI而只希望對核心的業務邏輯進行單元測試,如果沒有分層,UI與業務邏輯耦合,就會使單元測試無法準確命中目標甚至寸步難行。 5.2 抽象設計
抽象是增強軟件擴展性的一把利劍,主板廠商很早就把抽象應用自如了。比如主板上的USB接口,并不針對某一種具體的設備,而只定義了USB標準:接口尺寸、電流、電壓、數據傳輸協議等,然后依據這個標準生產主板。USB標準即抽象,主板廠商通過抽象獲得了對無限種類USB設備的擴展支持。
因為有了USB標準,所以很容易就可以設計生產一個USB測試工裝,這個工裝就類似于單元測試中的測試替身,主板廠商在測試時,并不需要外接一個用戶經常使用的U盤或USB鍵盤,而只需要外接一個USB測試工裝即可完成測試,并且這個測試工裝可以在符合USB基本標準的前提下按測試需求設計生產,比如只需要按數據傳輸標準接收數據即可而并不需要真正的存儲數據。 5.3 依賴注入
當發生火警時,消防通道的暢通保障了救援。在單元測試中,依賴注入保障了代碼的可測試。由此可見,依賴注入在可測試性編碼中的重要性。
如果所有職業按成就感進行排名的話,我想軟件開發一定是名列前茅的,因為大多數時候軟件開發人員扮演的就是“上帝角色”,他們可以隨時new一切需要的對象。但在依賴注入模式下,上帝需要從“自己創造”轉變為“習慣組裝”。 6 可測試性編碼
Google的研發工程師寫了一篇關于軟件可測試性的文章《Guide: Writing Testable Code》,覺得里面的代碼示例比較具有代表性,摘錄并整理簡化了代碼(可不關注語法細節,當作偽代碼來看)如下: 6.1 注入協作對象
難以測試的代碼示例:
易于測試的代碼示例:
6.2 不要依賴靜態方法
難以測試的代碼示例:
易于測試的代碼示例:
6.3 不要依賴全局變量
難以測試的代碼示例:
易于測試的代碼示例:
6.4 不要為了測試而測試
難以測試的代碼示例:
易于測試的代碼示例:
7 使用測試框架
7.1 GTest簡介
測試框架為我們提供了測試用例管理、斷言、參數化、用例執行等系列通用功能,使我們可以專注于測試用例本身業務邏輯的處理。在C/C++編程中,GTest當前最流行的單元測試框架,它由Google公司發布,支持跨平臺(Linux、Windows、MacOS),GTest官方倉庫地址為:https://github.com/google/googletest 7.2 使用GTest編寫單元測試用例
GTest框架會自動執行所有單元測試用例(由TEST、TEST_F等宏定義),一個單元測試用例類似于一個函數,其中第一個參數為測試套件名稱,測試套件就是一系列單元測試用例的集合,第二個參數為單元測試用例名稱,如上代碼所示。
在實際項目中,通常相同類型的多個測試用例需要相同的初始化和清理過程,或需要共用一些資源。此時就可以使用自定義測試套件方式,如上代碼所示。 7.3 單元測試覆蓋率統計
單元測試覆蓋率通常指的是行覆蓋率,其計算規則為:分母為被測項目有效代碼(排除空白、注釋等無效行)的總行數,分子為被單元測試用例執行到的行數,由此計算的比例為單元測試行覆蓋率。華為大多軟件項目對外宣稱的單元測試行覆蓋率為70%,根據我的經驗,這是一個相當高的比例了。 有很多統計單元測試覆蓋率的工具,比如針對C++的 OpenCppCoverage ,安裝后通過一條命令即可生成HTML可視化的單元測試覆蓋率統計報表:
8 單元測試的成敗關鍵
8.1 時間與成本預算
決定在項目中實施單元測試前,需要與項目經理充分溝通項目時間周期與成本,因為單元測試需要增加開發工程師在編碼階段的時間投入,這個比例大致在0.5~1.0之間。即假如某個功能的編碼時間是10天,那么需要增加大約5-10天來完成單元測試。同時單元測試并非一勞永逸,后續當被測試的業務代碼發生變更,與之對應的單元測試用例也需要同步變更。因此獲得相應的項目資源預算對單元測試的成敗至關重要,如果沒有給到開發人員相對充裕的時間,但又要求他們達成單元測試指標,就會導致開發人員認為單元測試擠占了功能開發時間,從而排斥單元測試。 8.2 在軟件架構設計階段整體考慮可測試性(架構師)
可測試性架構設計是達成單元測試在技術層面最重要的環節,好比房屋裝修,如果軟裝都完成了,冰箱彩電空調擺放就位,才發現忘了走電源線,那么補救成本就非常高了。 8.3 在編碼階段具備可測試性意識(開發工程師)
除了架構設計提前考慮對單元測試的支持,軟件編碼亦是如此,開發人員在編寫代碼前應提前了解單元測試,以不至于編寫出來的代碼不能或難以進行單元測試。比如全局變量滿天飛導致測試用例之間相互影響,類中的協作對象完全不使用依賴注入導致測試用例無從下手,等等。 9 后記
本文介紹了單元測試的基本概念,以及結合實際項目,分享了單元測試實施要點。是對自己項目過程的總結,也希望對有需要的同學有所幫助。 最后做一點補充,實施單元測試大致分為兩類,我稱之為主動單元測試和被動單元測試,主動單元測試,是以提升代碼質量和軟件架構為目的,由內部主動發起,實施過程中會同步優化軟件架構、提升代碼可測試性。而被動單元測試由外部驅使,比如來自客戶或市場的外部要求,它以覆蓋率為唯一目標,通常會借助一些商業工具(比如Tessy),自動生成單元測試用例與完成打樁,它不需要修改源程序代碼,當然也不會提升軟件的架構質量。本文所描述的,以及我個人比較推崇的為主動單元測試。 <全文完> ?轉自https://www.cnblogs.com/wubayue/p/18760269 該文章在 2025/3/10 16:06:07 編輯過 |
關鍵字查詢
相關文章
正在查詢... |