官术网_书友最值得收藏!

改善單元測(cè)試的新方法

我們?yōu)槭裁匆獙憜卧獪y(cè)試?

“滿足需求”是所有軟件存在的必要條件,單元測(cè)試一定是為它服務(wù)的。從這一點(diǎn)出發(fā),我們可以總結(jié)出寫單元測(cè)試的兩個(gè)動(dòng)機(jī):驅(qū)動(dòng)(如TDD)和驗(yàn)證功能實(shí)現(xiàn)。另外,軟件需求易變的特征決定了修改代碼成為必然,在這種情況下,單元測(cè)試能保護(hù)已有的功能不被破壞。

基于以上兩點(diǎn)共識(shí),我們看看傳統(tǒng)的單元測(cè)試有什么特征?

基于用例的測(cè)試(By Example)

單元測(cè)試最常見的套路就是Given,When,Then三部曲。

  • Given:初始狀態(tài)或前置條件
  • When:行為發(fā)生
  • Then:斷言結(jié)果

編寫單元測(cè)試時(shí),我們會(huì)精心準(zhǔn)備(Given)一組輸入數(shù)據(jù),然后在調(diào)用行為后,斷言返回的結(jié)果與預(yù)期相符。這種基于用例的測(cè)試方式在開發(fā)(包括TDD)過程中十分好用。因?yàn)樗逦囟x了輸入輸出,而且大部分情況下體量都很小、容易理解。

但這樣的測(cè)試方式也有壞處。

第一是測(cè)試的意圖。用例太過具體,我們就很容易忽略自己的測(cè)試意圖。比如我曾經(jīng)看過有人在寫計(jì)算器kata程序的時(shí)候,將其中的一個(gè)測(cè)試命名為return 3 when add 1 and 2,這樣的命名其實(shí)掩蓋了測(cè)試用例背后的真實(shí)意圖,傳入兩個(gè)整型參數(shù),調(diào)用add方法之后得到的結(jié)果應(yīng)該是兩者之和。我們常說測(cè)試即文檔,既然是文檔,就應(yīng)該明確描述待測(cè)方法的行為,而不是陳述一個(gè)例子。

第二是測(cè)試完備性。因?yàn)槭∈率⌒牟⑶一貓?bào)率高,我們更樂于寫happy path的代碼。盡管出于職業(yè)道德,我們也會(huì)找一個(gè)明顯的異常路徑進(jìn)行測(cè)試,不過這還遠(yuǎn)遠(yuǎn)不夠。

生成式測(cè)試

為了輔助單元測(cè)試改善這兩點(diǎn),我這里介紹另一種測(cè)試方式“生成式測(cè)試”(Generative Testing,也稱Property-Based Testing)。這種測(cè)試方式會(huì)基于輸入假設(shè)輸出,生成許多可能的數(shù)據(jù)來驗(yàn)證假設(shè)的正確性。

對(duì)此,我們換種思路。假設(shè)我們不寫具體的測(cè)試用例,而是直接描述意圖,那么問題也就迎刃而解了。想法很美好,但如何實(shí)踐Given,When,Then呢?答案是讓程序自動(dòng)生成入?yún)⒉Ⅱ?yàn)證結(jié)果。這也就引出“生成式測(cè)試”的概念:我們先聲明傳入數(shù)據(jù)可能的情況,然后使用生成器生成符合入?yún)⑶闆r的數(shù)據(jù),調(diào)用待測(cè)方法,最后進(jìn)行驗(yàn)證。

Given階段

Clojure 1.9(Alpha)新內(nèi)置的Clojure.spec可以很輕松地做到這點(diǎn):

首先,我們嘗試聲明兩個(gè)參數(shù)可能出現(xiàn)的情況或者稱為“規(guī)格”,即參數(shù)a和b都是整數(shù)。然后調(diào)用生成器產(chǎn)生一對(duì)整數(shù)。整個(gè)分析和構(gòu)造的過程中,都沒有涉及具體的數(shù)據(jù),這樣會(huì)強(qiáng)制我們揣摩輸入數(shù)據(jù)可能的模樣,而且也能避免測(cè)試意圖被掩蓋掉,正如前面所說,return 3 when add 1 and 2并不代表什么,return the sum of two integers才具有普遍意義。

Then階段

數(shù)據(jù)是生成了,待測(cè)方法也可以調(diào)用,但是Then這個(gè)斷言階段又讓人頭疼了,因?yàn)槲覀兏緵]法預(yù)知生成的數(shù)據(jù),也就無法知道正確的結(jié)果,怎么斷言?

以定義好的加法運(yùn)算為例:

我們嘗試把斷言改成一個(gè)全稱命題:任取兩個(gè)整數(shù)a、b,a和b加起來的結(jié)果總是a,b之和。借助test.check,我們?cè)贑lojure可以這樣表達(dá):

不過,我們把a(bǔ)dd方法的實(shí)現(xiàn)(+ a b)寫入斷言,這幾乎喪失了單元測(cè)試的基本意義。換一種斷言方式,我們使用加法的逆運(yùn)算進(jìn)行描述:任取兩個(gè)整數(shù),把a(bǔ)和b加起來的結(jié)果減去a總會(huì)得到b。

我們通過程序陳述了一個(gè)已知的真命題。變換以后,就可以使用quick-check對(duì)多組生成的整數(shù)進(jìn)行測(cè)試。

測(cè)試結(jié)果表明,剛才運(yùn)行了100組測(cè)試,并且都通過了。理論上,程序可以生成無數(shù)的測(cè)試數(shù)據(jù)來驗(yàn)證add方法的正確性。即便不能窮盡,我們也獲得一組統(tǒng)計(jì)上的數(shù)字,而不僅僅是幾個(gè)純手工挑選的用例。

至于第二個(gè)問題,首先得明確測(cè)試是無法做到完備的。很多指導(dǎo)方法保證使用較少的用例做到有效覆蓋,比如等價(jià)類、邊界值、判定表、因果圖和pairwise等。但是在實(shí)際使用過程當(dāng)中,依然存在問題。舉個(gè)例子,假如我們有一個(gè)接收自然數(shù)并直接返回這個(gè)參數(shù)的方法identity-nat,那么對(duì)于輸入?yún)?shù)而言,全體自然數(shù)都互為等價(jià)類,其中的一個(gè)有效等價(jià)類可以是自然數(shù)1;假定入?yún)⒈幌薅ㄔ谡麛?shù)范圍,我們很容易找到一個(gè)無效等價(jià)類,比如-1。用Clojure測(cè)試代碼表現(xiàn)出來:

不過如果有人修改了方法identity-nat的實(shí)現(xiàn),單獨(dú)處理入?yún)?的情況,這個(gè)測(cè)試還是能夠照常通過。也就是說,實(shí)現(xiàn)發(fā)生改變,基于等價(jià)類的測(cè)試有可能起不到防護(hù)作用。當(dāng)然你完全可以反駁:規(guī)則改變導(dǎo)致等價(jià)類也需要重新定義。道理確實(shí)如此,但是反過來想想,我們寫測(cè)試的目的不正是構(gòu)建一張安全網(wǎng)嗎?我們信任測(cè)試能在代碼變動(dòng)時(shí)給予警告,但此處它失信了,這就尷尬了。

如果使用生成式測(cè)試,我們規(guī)定:任取一個(gè)自然數(shù)a,在其上調(diào)用identity-nat的結(jié)果總是返回a。

這個(gè)測(cè)試嘗試對(duì)100組生成的自然數(shù)(nat-int?)進(jìn)行測(cè)試,但首次運(yùn)行就發(fā)現(xiàn)代碼發(fā)生過變動(dòng)。失敗的數(shù)據(jù)是0,而且還給出了最小失敗集[0]。拿著這個(gè)最小失敗集,我們就可以快速地重現(xiàn)失敗用例,從而修正。

當(dāng)然也存在這樣的可能:在一次運(yùn)行中,我們的測(cè)試無法發(fā)現(xiàn)失敗的用例。但是,如果100個(gè)測(cè)試用例都通過了,至少表明我們程序?qū)τ?00個(gè)隨機(jī)的自然數(shù)都是正確的,和基于用例的測(cè)試相比,這就如同編織出一道更加緊密的安全網(wǎng),網(wǎng)孔越小,漏掉的情況也越少。

Clojure語言之父里奇·海奇(Rich Hickey)推崇Simple Made Easy哲學(xué),受其影響生成式測(cè)試在Clojure.spec中有更為簡(jiǎn)約的表達(dá)。以上述為例:

fdef宏定義了方法identity-nat的規(guī)格,默認(rèn)情況下會(huì)基于參數(shù)的規(guī)格生成1000組數(shù)據(jù)進(jìn)行生成式測(cè)試。除了這一好處,它還提供部分類型檢查的功能。

再談TDD

TDD(測(cè)試驅(qū)動(dòng)開發(fā))是一種驅(qū)動(dòng)代碼實(shí)現(xiàn)和設(shè)計(jì)的過程。我們說要先有測(cè)試,再去實(shí)現(xiàn);保證實(shí)現(xiàn)功能的前提下,重構(gòu)代碼以達(dá)到較好的設(shè)計(jì)。整個(gè)過程就好比演繹推理,測(cè)試就是其中的證明步驟,而最終實(shí)現(xiàn)的功能則是證明的結(jié)果。

對(duì)于開發(fā)人員而言,基于用例的測(cè)試方式是友好的,因?yàn)樗芎?jiǎn)單直接地表達(dá)實(shí)現(xiàn)的功能并保證其正確性。一旦進(jìn)入紅、綠、重構(gòu)的節(jié)(guai)奏(quan),開發(fā)人員根本停不下來,仿佛遁入一種心流狀態(tài)。只不過問題是,基于用例驅(qū)動(dòng)出來的實(shí)現(xiàn)可能并不是恰好通過的。我們常常會(huì)發(fā)現(xiàn),在寫完上組測(cè)試用例的實(shí)現(xiàn)之后,無需任何改動(dòng),下組測(cè)試照常能運(yùn)行通過。換句話說,實(shí)現(xiàn)代碼可能做了多余的事情而我們卻渾然不知。在這種情況下,我們可以利用生成式測(cè)試準(zhǔn)備大量符合規(guī)格的數(shù)據(jù)探測(cè)程序,以此檢查程序的健壯性,讓缺陷無處遁形。

凡是想到的情況都能測(cè)試,但是想不到情況也需要測(cè)試,這才是生成式測(cè)試的價(jià)值所在。有人把TDD概念化為“展示你的功能”(Show your work),而把生成式測(cè)試歸納為“檢查你的功能“(Check your work),我深以為然。

主站蜘蛛池模板: 上思县| 拜城县| 通河县| 宝丰县| 台东县| 安仁县| 资中县| 攀枝花市| 龙南县| 三台县| 西乌| 苗栗县| 庐江县| 梁山县| 定远县| 庆元县| 金华市| 福建省| 宁城县| 临西县| 通辽市| 宝山区| 旅游| 黄平县| 宜兴市| 油尖旺区| 白河县| 罗甸县| 庄河市| 上思县| 湾仔区| 宽甸| 天台县| 澄城县| 门头沟区| 莲花县| 余干县| 苏尼特左旗| 运城市| 长乐市| 旺苍县|