首先為什么要寫(xiě)單元測(cè)試?
“滿足需求”是所有軟件存在的必要條件,單元測(cè)試一定是為它服務(wù)的。從這一點(diǎn)出發(fā)荞估,我們可以總結(jié)出寫(xiě)單元測(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è)試最常見(jiàn)的套路就是Given屯阀、When缅帘、Then三部曲。
- Given:初始狀態(tài)或前置條件
- When:行為發(fā)生
- Then:斷言結(jié)果
編寫(xiě)時(shí)难衰,我們會(huì)精心準(zhǔn)備(Given)一組輸入數(shù)據(jù)钦无,然后在調(diào)用行為后,斷言返回的結(jié)果與預(yù)期相符盖袭。這種基于用例的測(cè)試方式在開(kāi)發(fā)(包括TDD)過(guò)程中十分好用失暂。因?yàn)樗逦囟x了輸入輸出,而且大部分情況下體量都很小鳄虱、容易理解弟塞。
但這樣的測(cè)試方式也有壞處。
- 第一點(diǎn)在于測(cè)試的意圖拙已。用例太過(guò)具體决记,我們就很容易忽略自己的測(cè)試意圖。比如我曾經(jīng)看過(guò)有人在寫(xiě)計(jì)算器kata程序的時(shí)候倍踪,將其中的一個(gè)測(cè)試命名為“return 3 when add 1 and 2”系宫,這樣的命名其實(shí)掩蓋了測(cè)試用例背后的真實(shí)意圖——傳入兩個(gè)整型參數(shù),調(diào)用add方法之后得到的結(jié)果應(yīng)該是兩者之和建车。我們常說(shuō)測(cè)試即文檔扩借,既然是文檔就應(yīng)該明確描述待測(cè)方法的行為,而不是陳述一個(gè)例子癞志。
- 第二點(diǎn)在于測(cè)試完備性往枷。因?yàn)槭∈率⌒牟⑶一貓?bào)率高,我們更樂(lè)于寫(xiě)happy path的代碼凄杯。盡管出于職業(yè)道德错洁,我們也會(huì)找一個(gè)明顯的異常路徑進(jìn)行測(cè)試,不過(guò)這還遠(yuǎn)遠(yuǎn)不夠戒突。
為了輔助單元測(cè)試改善這兩點(diǎn)屯碴。我這里介紹另一種測(cè)試方式——生成式測(cè)試(Generative Testing,也稱Property-Based Testing)膊存。這種測(cè)試方式會(huì)基于輸入假設(shè)輸出导而,并且生成許多可能的數(shù)據(jù)來(lái)驗(yàn)證假設(shè)的正確性。
生成式測(cè)試
對(duì)于第一個(gè)問(wèn)題隔崎,我們換種思路思考一下今艺。假設(shè)我們不寫(xiě)具體的測(cè)試用例,而是直接描述意圖爵卒,那么問(wèn)題也就迎刃而解了虚缎。想法很美好,但如何實(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):
;; 定義輸入?yún)?shù)的可能情況:兩個(gè)整型參數(shù)
(s/def ::add-operators (s/cat :a int? :b int?))
;; 嘗試生成數(shù)據(jù)
(gen/generate (s/gen ::add-operators))
;; 生成的數(shù)據(jù)
-> (1 -122)
首先偎谁,我們嘗試聲明兩個(gè)參數(shù)可能出現(xiàn)的情況或者稱為規(guī)格(specification),即參數(shù)a和b都是整數(shù)携栋。然后調(diào)用生成器產(chǎn)生一對(duì)整數(shù)搭盾。整個(gè)分析和構(gòu)造的過(guò)程中,都沒(méi)有涉及具體的數(shù)據(jù)婉支,這樣會(huì)強(qiáng)制我們揣摩輸入數(shù)據(jù)可能的模樣鸯隅,而且也能避免測(cè)試意圖被掩蓋掉——正如前面所說(shuō),return 3 when add 1 and 2并不代表什么向挖,return the sum of two integers才具有普遍意義蝌以。
Then階段
數(shù)據(jù)是生成了,待測(cè)方法也可以調(diào)用何之,但是Then這個(gè)斷言階段又讓人頭疼了跟畅,因?yàn)槲覀兏緵](méi)法預(yù)知生成的數(shù)據(jù),也就無(wú)法知道正確的結(jié)果溶推,怎么斷言徊件?
拿定義好的加法運(yùn)算為例:
(defn add [a b]
(+ a b))
我們嘗試把斷言改成一個(gè)全稱命題: 任取兩個(gè)整數(shù)a、b蒜危,a和b加起來(lái)的結(jié)果總是a虱痕、b之和。 借助test.check辐赞,我們?cè)贑lojure可以這樣表達(dá):
(def test-add
(prop/for-all [a (gen/int)
b (gen/int)]
(= (add a b) (+ a b))))
不過(guò)部翘,我們把a(bǔ)dd方法的實(shí)現(xiàn)(+ a b)寫(xiě)到了斷言里,這幾乎喪失了單元測(cè)試的基本意義响委。換一種斷言方式新思,我們使用加法的逆運(yùn)算進(jìn)行描述: 任取兩個(gè)整數(shù),把a(bǔ)和b加起來(lái)的結(jié)果減去a總會(huì)得到b赘风。
(def test-add
(prop/for-all [a (gen/int)
b (gen/int)]
(= (- (add a b) a) b))))
我們通過(guò)程序陳述了一個(gè)已知的真命題夹囚。變換以后,就可以使用quick-check對(duì)多組生成的整數(shù)進(jìn)行測(cè)試邀窃。
;; 隨機(jī)生成100組數(shù)據(jù)測(cè)試add方法
(tc/quick-check 100 test-add)
;; 測(cè)試結(jié)果
-> {:result true, :num-tests 100, :seed 1477285296502}
測(cè)試結(jié)果表明崔兴,剛才運(yùn)行了100組測(cè)試,并且都通過(guò)了。理論上敲茄,程序可以生成無(wú)數(shù)的測(cè)試數(shù)據(jù)來(lái)驗(yàn)證add方法的正確性。即便不能窮盡山析,我們也獲得一組統(tǒng)計(jì)上的數(shù)字堰燎,而不僅僅是幾個(gè)純手工挑選的用例。
至于第二個(gè)問(wèn)題笋轨,首先得明確測(cè)試是無(wú)法做到完備的秆剪。很多指導(dǎo)方法保證使用較少的用例做到有效覆蓋,比如:等價(jià)類(lèi)爵政、邊界值仅讽、判定表、因果圖钾挟、pairwise等等洁灵。但是在實(shí)際使用過(guò)程當(dāng)中,依然存在問(wèn)題掺出。舉個(gè)例子徽千,假如我們有一個(gè)接收自然數(shù)并直接返回這個(gè)參數(shù)的方法identity-nat,那么對(duì)于輸入?yún)?shù)而言汤锨,全體自然數(shù)都互為等價(jià)類(lèi)双抽,其中的一個(gè)有效等價(jià)類(lèi)可以是自然數(shù)1;假定入?yún)⒈幌薅ㄔ谡麛?shù)范圍闲礼,我們很容易找到一個(gè)無(wú)效等價(jià)類(lèi)牍汹,比如-1。 用Clojure測(cè)試代碼表現(xiàn)出來(lái):
(deftest test-with-identity-nat
(testing "identity of natural integers"
(is (= 1 (identity-nat 1))))
(testing "throw exception for non-natural integers"
(is (thrown? RuntimeException (identity-nat -1)))))
不過(guò)如果有人修改了方法identity-nat的實(shí)現(xiàn)柬泽,單獨(dú)處理入?yún)?的情況慎菲,這個(gè)測(cè)試還是能夠照常通過(guò)。也就是說(shuō)聂抢,實(shí)現(xiàn)發(fā)生改變钧嘶,基于等價(jià)類(lèi)的測(cè)試有可能起不到防護(hù)作用。當(dāng)然你完全可以反駁:規(guī)則改變導(dǎo)致等價(jià)類(lèi)也需要重新定義琳疏。道理確實(shí)如此有决,但是反過(guò)來(lái)想想,我們寫(xiě)測(cè)試的目的不正是構(gòu)建一張安全網(wǎng)嗎空盼?我們信任測(cè)試能在代碼變動(dòng)時(shí)給予警告书幕,但此處它失信了,這就尷尬了揽趾。
如果使用生成式測(cè)試台汇,我們規(guī)定:
任取一個(gè)自然數(shù)a,在其上調(diào)用identity-nat的結(jié)果總是返回a。
(def test-identity-nat
(prop/for-all [a (s/gen nat-int?)]
(= a (identity-nat a))))
(tc/quick-check 100 test-identity-nat)
-> {:result false,
:seed 1477362396044,
:failing-size 0,
:num-tests 1,
:fail [0],
:shrunk {:total-nodes-visited 0,
:depth 0,
:result false,
:smallest [0]}}
這個(gè)測(cè)試嘗試對(duì)100組生成的自然數(shù)(nat-int?)進(jìn)行測(cè)試苟呐,但首次運(yùn)行就發(fā)現(xiàn)代碼發(fā)生過(guò)變動(dòng)痒芝。失敗的數(shù)據(jù)是0,而且還給出了最小失敗集[0]牵素。拿著這個(gè)最小失敗集严衬,我們就可以快速地重現(xiàn)失敗用例,從而修正笆呆。
當(dāng)然也存在這樣的可能:在一次運(yùn)行中请琳,我們的測(cè)試無(wú)法發(fā)現(xiàn)失敗的用例。但是赠幕,如果100個(gè)測(cè)試用例都通過(guò)了俄精,至少表明我們程序?qū)τ?00個(gè)隨機(jī)的自然數(shù)都是正確的,和基于用例的測(cè)試相比榕堰,這就如同編織出一道更加緊密的安全網(wǎng)——網(wǎng)孔越小竖慧,漏掉的情況也越少。
Clojure語(yǔ)言之父Rich Hickey推崇Simple Made Easy哲學(xué)局冰,受其影響生成式測(cè)試在Clojure.spec中有更為簡(jiǎn)約的表達(dá)测蘑。以上述為例:
(s/fdef identity-nat
:args (s/cat :a nat-int?) ; 輸入?yún)?shù)的規(guī)格
:ret nat-int? ; 返回結(jié)果的規(guī)格
:fn #(= (:ret %) (-> % :args :a))) ; 入?yún)⒑统鰠⒅g的約束
(stest/check `identity-nat)
fdef宏定義了方法identity-nat的規(guī)格,默認(rèn)情況下會(huì)基于參數(shù)的規(guī)格生成1000組數(shù)據(jù)進(jìn)行生成式測(cè)試康二。除了這一好處碳胳,它還提供部分類(lèi)型檢查的功能。
再談TDD
如果對(duì)軟件測(cè)試沫勿、接口測(cè)試挨约、自動(dòng)化測(cè)試、性能測(cè)試产雹、LR腳本開(kāi)發(fā)诫惭、面試經(jīng)驗(yàn)交流。感興趣可以175317069蔓挖,群內(nèi)會(huì)有不定期的發(fā)放免費(fèi)的資料鏈接夕土,這些資料都是從各個(gè)技術(shù)網(wǎng)站搜集、整理出來(lái)的瘟判,如果你有好的學(xué)習(xí)資料可以私聊發(fā)我怨绣,我會(huì)注明出處之后分享給大家。
TDD(測(cè)試驅(qū)動(dòng)開(kāi)發(fā))是一種驅(qū)動(dòng)代碼實(shí)現(xiàn)和設(shè)計(jì)的過(guò)程拷获。我們說(shuō)要先有測(cè)試篮撑,再去實(shí)現(xiàn);保證實(shí)現(xiàn)功能的前提下匆瓜,重構(gòu)代碼以達(dá)到較好的設(shè)計(jì)赢笨。整個(gè)過(guò)程就好比演繹推理未蝌,測(cè)試就是其中的證明步驟,而最終實(shí)現(xiàn)的功能則是證明的結(jié)果茧妒。
對(duì)于開(kāi)發(fā)人員而言萧吠,基于用例的測(cè)試方式是友好的,因?yàn)樗芎?jiǎn)單直接地表達(dá)實(shí)現(xiàn)的功能并保證其正確性嘶伟。一旦進(jìn)入紅怎憋、綠、重構(gòu)的節(jié)(guai)奏(quan)九昧,開(kāi)發(fā)人員根本停不下來(lái),仿佛遁入一種心流狀態(tài)毕匀。只不過(guò)問(wèn)題是铸鹰,基于用例驅(qū)動(dòng)出來(lái)的實(shí)現(xiàn)可能并不是恰好通過(guò)的。我們常常會(huì)發(fā)現(xiàn)皂岔,在寫(xiě)完上組測(cè)試用例的實(shí)現(xiàn)之后蹋笼,無(wú)需任何改動(dòng),下組測(cè)試照常能運(yùn)行通過(guò)躁垛。換句話說(shuō)剖毯,實(shí)現(xiàn)代碼可能做了多余的事情而我們卻渾然不知。在這種情況下教馆,我們可以利用生成式測(cè)試準(zhǔn)備大量符合規(guī)格的數(shù)據(jù)探測(cè)程序逊谋,以此檢查程序的健壯性,讓缺陷無(wú)處遁形土铺。
凡是想到的情況都能測(cè)試胶滋,但是想不到情況也需要測(cè)試,這才是生成式測(cè)試的價(jià)值所在悲敷。有人把TDD概念化為“展示你的功能”(Show your work)究恤,而把生成式測(cè)試歸納為“檢查你的功能“(Check your work),我深以為然后德。
小結(jié)
回到我們寫(xiě)單元測(cè)試的動(dòng)機(jī)上:
1部宿、驅(qū)動(dòng)和驗(yàn)證功能實(shí)現(xiàn);
2瓢湃、保護(hù)已有的功能不被破壞理张。
基于用例的單元測(cè)試和生成式測(cè)試在這兩點(diǎn)上是相輔相成的。我們可以借助它們盡可能早地發(fā)現(xiàn)更多的缺陷箱季,避免它們逃逸到生產(chǎn)環(huán)境涯穷。
Clojure.spec是Clojure內(nèi)置的一個(gè)新特性,它允許開(kāi)發(fā)人員將數(shù)據(jù)結(jié)構(gòu)用類(lèi)型和其他驗(yàn)證條件(例如允許的取值范圍)進(jìn)行封裝藏雏。這種數(shù)據(jù)結(jié)構(gòu)一旦建立拷况,Clojure就能利用這種規(guī)格來(lái)為程序員提供大量的便利:自動(dòng)生成的測(cè)試代碼作煌、合法性驗(yàn)證、析構(gòu)數(shù)據(jù)結(jié)構(gòu)等等赚瘦。Clojure.spec提供方法很有前景粟誓,它可以讓開(kāi)發(fā)者在需要的時(shí)候,就能從類(lèi)型和取值范圍中獲益起意。
另外鹰服,除了Clojure,其它語(yǔ)言也有相應(yīng)的生成式測(cè)試的框架揽咕,你不妨在自己的項(xiàng)目中試一試缘回。