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