轉(zhuǎn)帖|使用教程|編輯:蔣永|2016-11-18 13:36:32.000|閱讀 342 次
概述:“滿足需求”是所有軟件存在的必要條件,單元測試一定是為它服務(wù)的。從這一點(diǎn)出發(fā),我們可以總結(jié)出寫單元測試的兩個動機(jī):驅(qū)動(如:TDD)和驗(yàn)證功能實(shí)現(xiàn)。另外,軟件需求“易變”的特征決定了修改代碼成為必然,在這種情況下,單元測試能保護(hù)已有的功能不被破壞。
# 界面/圖表報(bào)表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
基于摘要中提到的兩點(diǎn)共識,讓我們看看傳統(tǒng)的單元測試有什么特征?
單元測試最常見的套路就是Given、When、Then三部曲。
編寫時,我們會精心準(zhǔn)備(Given)一組輸入數(shù)據(jù),然后在調(diào)用行為后,斷言返回的結(jié)果與預(yù)期相符。這種基于用例的測試方式在開發(fā)(包括TDD)過程中十分好用。因?yàn)樗逦囟x了輸入輸出,而且大部分情況下體量都很小、容易理解。
但這樣的測試方式也有壞處。
為了輔助單元測試改善這兩點(diǎn)。我這里介紹另一種測試方式——生成式測試(Generative Testing,也稱Property-Based Testing)。這種測試方式會基于輸入假設(shè)輸出,并且生成許多可能的數(shù)據(jù)來驗(yàn)證假設(shè)的正確性。
對于第一個問題,我們換種思路思考一下。假設(shè)我們不寫具體的測試用例,而是直接描述意圖,那么問題也就迎刃而解了。想法很美好,但如何實(shí)踐Given、When、Then呢?答案是讓程序自動生成入?yún)⒉Ⅱ?yàn)證結(jié)果。這也就引出“生成式測試”的概念——我們先聲明傳入數(shù)據(jù)可能的情況,然后使用生成器生成符合入?yún)⑶闆r的數(shù)據(jù),調(diào)用待測方法,最后進(jìn)行驗(yàn)證。
Clojure 1.9(Alpha)新內(nèi)置的Clojure.spec可以很輕松地做到這點(diǎn):
<code>;; 定義輸入?yún)?shù)的可能情況:兩個整型參數(shù) (s/def ::add-operators (s/cat :a int? :b int?)) ;; 嘗試生成數(shù)據(jù) (gen/generate (s/gen ::add-operators)) ;; 生成的數(shù)據(jù) -> (1 -122) </code>
首先,我們嘗試聲明兩個參數(shù)可能出現(xiàn)的情況或者稱為規(guī)格(specification),即參數(shù)a和b都是整數(shù)。然后調(diào)用生成器產(chǎn)生一對整數(shù)。整個分析和構(gòu)造的過程中,都沒有涉及具體的數(shù)據(jù),這樣會強(qiáng)制我們揣摩輸入數(shù)據(jù)可能的模樣,而且也能避免測試意圖被掩蓋掉——正如前面所說,return 3 when add 1 and 2并不代表什么,return the sum of two integers才具有普遍意義。
數(shù)據(jù)是生成了,待測方法也可以調(diào)用,但是Then這個斷言階段又讓人頭疼了,因?yàn)槲覀兏緵]法預(yù)知生成的數(shù)據(jù),也就無法知道正確的結(jié)果,怎么斷言?
拿定義好的加法運(yùn)算為例:
(defn add [a b]
(+ a b))
我們嘗試把斷言改成一個全稱命題: 任取兩個整數(shù)a、b,a和b加起來的結(jié)果總是a、b之和。 借助test.check,我們在Clojure可以這樣表達(dá):
(def test-add
(prop/for-all [a (gen/int)
b (gen/int)]
(= (add a b) (+ a b))))
不過,我們把a(bǔ)dd方法的實(shí)現(xiàn)(+ a b)寫到了斷言里,這幾乎喪失了單元測試的基本意義。換一種斷言方式,我們使用加法的逆運(yùn)算進(jìn)行描述: 任取兩個整數(shù),把a(bǔ)和b加起來的結(jié)果減去a總會得到b。
(def test-add
(prop/for-all [a (gen/int)
b (gen/int)]
(= (- (add a b) a) b))))
我們通過程序陳述了一個已知的真命題。變換以后,就可以使用quick-check對多組生成的整數(shù)進(jìn)行測試。
;; 隨機(jī)生成100組數(shù)據(jù)測試add方法
(tc/quick-check 100 test-add)
;; 測試結(jié)果
-> {:result true, :num-tests 100, :seed 1477285296502}
測試結(jié)果表明,剛才運(yùn)行了100組測試,并且都通過了。理論上,程序可以生成無數(shù)的測試數(shù)據(jù)來驗(yàn)證add方法的正確性。即便不能窮盡,我們也獲得一組統(tǒng)計(jì)上的數(shù)字,而不僅僅是幾個純手工挑選的用例。
至于第二個問題,首先得明確測試是無法做到完備的。很多指導(dǎo)方法保證使用較少的用例做到有效覆蓋,比如:等價(jià)類、邊界值、判定表、因果圖、pairwise等等。但是在實(shí)際使用過程當(dāng)中,依然存在問題。舉個例子,假如我們有一個接收自然數(shù)并直接返回這個參數(shù)的方法identity-nat,那么對于輸入?yún)?shù)而言,全體自然數(shù)都互為等價(jià)類,其中的一個有效等價(jià)類可以是自然數(shù)1;假定入?yún)⒈幌薅ㄔ谡麛?shù)范圍,我們很容易找到一個無效等價(jià)類,比如-1。 用Clojure測試代碼表現(xiàn)出來:
(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)))))
不過如果有人修改了方法identity-nat的實(shí)現(xiàn),單獨(dú)處理入?yún)?的情況,這個測試還是能夠照常通過。也就是說,實(shí)現(xiàn)發(fā)生改變,基于等價(jià)類的測試有可能起不到防護(hù)作用。當(dāng)然你完全可以反駁:規(guī)則改變導(dǎo)致等價(jià)類也需要重新定義。道理確實(shí)如此,但是反過來想想,我們寫測試的目的不正是構(gòu)建一張安全網(wǎng)嗎?我們信任測試能在代碼變動時給予警告,但此處它失信了,這就尷尬了。
如果使用生成式測試,我們規(guī)定:
任取一個自然數(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]}}
這個測試嘗試對100組生成的自然數(shù)(nat-int?)進(jìn)行測試,但首次運(yùn)行就發(fā)現(xiàn)代碼發(fā)生過變動。失敗的數(shù)據(jù)是0,而且還給出了最小失敗集[0]。拿著這個最小失敗集,我們就可以快速地重現(xiàn)失敗用例,從而修正。
當(dāng)然也存在這樣的可能:在一次運(yùn)行中,我們的測試無法發(fā)現(xiàn)失敗的用例。但是,如果100個測試用例都通過了,至少表明我們程序?qū)τ?00個隨機(jī)的自然數(shù)都是正確的,和基于用例的測試相比,這就如同編織出一道更加緊密的安全網(wǎng)——網(wǎng)孔越小,漏掉的情況也越少。
Clojure語言之父Rich Hickey推崇Simple Made Easy哲學(xué),受其影響生成式測試在Clojure.spec中有更為簡約的表達(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)情況下會基于參數(shù)的規(guī)格生成1000組數(shù)據(jù)進(jìn)行生成式測試。除了這一好處,它還提供部分類型檢查的功能。
TDD(測試驅(qū)動開發(fā))是一種驅(qū)動代碼實(shí)現(xiàn)和設(shè)計(jì)的過程。我們說要先有測試,再去實(shí)現(xiàn);保證實(shí)現(xiàn)功能的前提下,重構(gòu)代碼以達(dá)到較好的設(shè)計(jì)。整個過程就好比演繹推理,測試就是其中的證明步驟,而最終實(shí)現(xiàn)的功能則是證明的結(jié)果。
對于開發(fā)人員而言,基于用例的測試方式是友好的,因?yàn)樗芎唵沃苯拥乇磉_(dá)實(shí)現(xiàn)的功能并保證其正確性。一旦進(jìn)入紅、綠、重構(gòu)的節(jié)(guai)奏(quan),開發(fā)人員根本停不下來,仿佛遁入一種心流狀態(tài)。只不過問題是,基于用例驅(qū)動出來的實(shí)現(xiàn)可能并不是恰好通過的。我們常常會發(fā)現(xiàn),在寫完上組測試用例的實(shí)現(xiàn)之后,無需任何改動,下組測試照常能運(yùn)行通過。換句話說,實(shí)現(xiàn)代碼可能做了多余的事情而我們卻渾然不知。在這種情況下,我們可以利用生成式測試準(zhǔn)備大量符合規(guī)格的數(shù)據(jù)探測程序,以此檢查程序的健壯性,讓缺陷無處遁形。
凡是想到的情況都能測試,但是想不到情況也需要測試,這才是生成式測試的價(jià)值所在。有人把TDD概念化為“展示你的功能”(Show your work),而把生成式測試歸納為“檢查你的功能“(Check your work),我深以為然。
回到我們寫單元測試的動機(jī)上:
1、驅(qū)動和驗(yàn)證功能實(shí)現(xiàn);
2、保護(hù)已有的功能不被破壞。
基于用例的單元測試和生成式測試在這兩點(diǎn)上是相輔相成的。我們可以借助它們盡可能早地發(fā)現(xiàn)更多的缺陷,避免它們逃逸到生產(chǎn)環(huán)境。 ThoughtWorks 2016年11月份的技術(shù)雷達(dá)把Clojure.spec移到了工具象限的評估環(huán)中,這表明值得我們對它作一番探究。
Clojure.spec是Clojure內(nèi)置的一個新特性,它允許開發(fā)人員將數(shù)據(jù)結(jié)構(gòu)用類型和其他驗(yàn)證條件(例如允許的取值范圍)進(jìn)行封裝。這種數(shù)據(jù)結(jié)構(gòu)一旦建立,Clojure就能利用這種規(guī)格來為程序員提供大量的便利:自動生成的測試代碼、合法性驗(yàn)證、析構(gòu)數(shù)據(jù)結(jié)構(gòu)等等。Clojure.spec提供方法很有前景,它可以讓開發(fā)者在需要的時候,就能從類型和取值范圍中獲益。
另外,除了Clojure,其它語言也有相應(yīng)的生成式測試的框架,不妨在自己的項(xiàng)目中試一試。
本文轉(zhuǎn)自()
>>>>>查看更多測試分析相關(guān)資訊、產(chǎn)品
活動時間:11月1日-11月30日
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請務(wù)必注明出處、不得修改原文相關(guān)鏈接,如果存在內(nèi)容上的異議請郵件反饋至chenjj@fc6vip.cn