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