改善單元測試的新方法

我們?yōu)槭裁匆獙憜卧獪y試纺铭?

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

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

基于用例的測試(By Example)

單元測試最常見的套路就是Given样刷、When、Then三部曲览爵。

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

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

但這樣的測試方式也有壞處。

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

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

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

生成式測試

對于第一個(gè)問題枉证,我們換種思路思考一下矮男。假設(shè)我們不寫具體的測試用例,而是直接描述意圖室谚,那么問題也就迎刃而解了毡鉴。想法很美好,但如何實(shí)踐Given舞萄、When眨补、Then呢?答案是讓程序自動(dòng)生成入?yún)⒉Ⅱ?yàn)證結(jié)果倒脓。這也就引出“生成式測試”的概念——我們先聲明傳入數(shù)據(jù)可能的情況,然后使用生成器生成符合入?yún)⑶闆r的數(shù)據(jù)含思,調(diào)用待測方法崎弃,最后進(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)生一對整數(shù)。整個(gè)分析和構(gòu)造的過程中盆均,都沒有涉及具體的數(shù)據(jù)塞弊,這樣會(huì)強(qiáng)制我們揣摩輸入數(shù)據(jù)可能的模樣,而且也能避免測試意圖被掩蓋掉——正如前面所說泪姨,return 3 when add 1 and 2并不代表什么游沿,return the sum of two integers才具有普遍意義。

Then階段

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

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

(defn add [a b]
 (+ a b))

我們嘗試把斷言改成一個(gè)全稱命題:
任取兩個(gè)整數(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)行描述:
任取兩個(gè)整數(shù)厂庇,把a(bǔ)和b加起來的結(jié)果減去a總會(huì)得到b渠啊。

(def test-add
(prop/for-all [a (gen/int)
            b (gen/int)]
           (= (- (add a b) a) b))))

我們通過程序陳述了一個(gè)已知的真命題。變換以后权旷,就可以使用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ù)字,而不僅僅是幾個(gè)純手工挑選的用例鄙麦。

至于第二個(gè)問題典唇,首先得明確測試是無法做到完備的镊折。很多指導(dǎo)方法保證使用較少的用例做到有效覆蓋,比如:等價(jià)類介衔、邊界值恨胚、判定表、因果圖炎咖、pairwise等等赃泡。但是在實(shí)際使用過程當(dāng)中,依然存在問題乘盼。舉個(gè)例子升熊,假如我們有一個(gè)接收自然數(shù)并直接返回這個(gè)參數(shù)的方法identity-nat,那么對于輸入?yún)?shù)而言蹦肴,全體自然數(shù)都互為等價(jià)類僚碎,其中的一個(gè)有效等價(jià)類可以是自然數(shù)1;假定入?yún)⒈幌薅ㄔ谡麛?shù)范圍阴幌,我們很容易找到一個(gè)無效等價(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)?的情況渊抽,這個(gè)測試還是能夠照常通過。也就是說议忽,實(shí)現(xiàn)發(fā)生改變懒闷,基于等價(jià)類的測試有可能起不到防護(hù)作用。當(dāng)然你完全可以反駁:規(guī)則改變導(dǎo)致等價(jià)類也需要重新定義栈幸。道理確實(shí)如此愤估,但是反過來想想,我們寫測試的目的不正是構(gòu)建一張安全網(wǎng)嗎速址?我們信任測試能在代碼變動(dòng)時(shí)給予警告玩焰,但此處它失信了,這就尷尬了芍锚。

如果使用生成式測試昔园,我們規(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è)測試嘗試對100組生成的自然數(shù)(nat-int?)進(jìn)行測試默刚,但首次運(yùn)行就發(fā)現(xiàn)代碼發(fā)生過變動(dòng)。失敗的數(shù)據(jù)是0逃魄,而且還給出了最小失敗集[0]荤西。拿著這個(gè)最小失敗集,我們就可以快速地重現(xiàn)失敗用例,從而修正皂冰。

當(dāng)然也存在這樣的可能:在一次運(yùn)行中店展,我們的測試無法發(fā)現(xiàn)失敗的用例养篓。但是秃流,如果100個(gè)測試用例都通過了,至少表明我們程序?qū)τ?00個(gè)隨機(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)情況下會(huì)基于參數(shù)的規(guī)格生成1000組數(shù)據(jù)進(jìn)行生成式測試轩端。除了這一好處,它還提供部分類型檢查的功能逝变。

再談TDD

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

對于開發(fā)人員而言掺栅,基于用例的測試方式是友好的烙肺,因?yà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)熏瞄,在寫完上組測試用例的實(shí)現(xiàn)之后,無需任何改動(dòng)谬以,下組測試照常能運(yùn)行通過强饮。換句話說,實(shí)現(xiàn)代碼可能做了多余的事情而我們卻渾然不知为黎。在這種情況下邮丰,我們可以利用生成式測試準(zhǔn)備大量符合規(guī)格的數(shù)據(jù)探測程序行您,以此檢查程序的健壯性,讓缺陷無處遁形剪廉。

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

小結(jié)

回到我們寫單元測試的動(dòng)機(jī)上:
驅(qū)動(dòng)和驗(yàn)證功能實(shí)現(xiàn)捞蚂;
保護(hù)已有的功能不被破壞。
基于用例的單元測試和生成式測試在這兩點(diǎn)上是相輔相成的跷究。我們可以借助它們盡可能早地發(fā)現(xiàn)更多的缺陷姓迅,避免它們逃逸到生產(chǎn)環(huán)境。
ThoughtWorks 2016年11月份的技術(shù)雷達(dá)把Clojure.spec移到了工具象限的評估環(huán)中俊马,這表明值得我們對它作一番探究丁存。

Clojure.spec是Clojure內(nèi)置的一個(gè)新特性,它允許開發(fā)人員將數(shù)據(jù)結(jié)構(gòu)用類型和其他驗(yàn)證條件(例如允許的取值范圍)進(jìn)行封裝潭袱。這種數(shù)據(jù)結(jié)構(gòu)一旦建立柱嫌,Clojure就能利用這種規(guī)格來為程序員提供大量的便利:自動(dòng)生成的測試代碼、合法性驗(yàn)證屯换、析構(gòu)數(shù)據(jù)結(jié)構(gòu)等等编丘。Clojure.spec提供方法很有前景,它可以讓開發(fā)者在需要的時(shí)候彤悔,就能從類型和取值范圍中獲益嘉抓。

另外,除了Clojure晕窑,其它語言也有相應(yīng)的生成式測試的框架抑片,你不妨在自己的項(xiàng)目中試一試。


更多精彩洞見杨赤,請關(guān)注微信公眾號:ThoughtWorks

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末敞斋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子疾牲,更是在濱河造成了極大的恐慌植捎,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阳柔,死亡現(xiàn)場離奇詭異焰枢,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門济锄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來暑椰,“玉大人,你說我怎么就攤上這事荐绝∫黄” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵很泊,是天一觀的道長角虫。 經(jīng)常有香客問我沾谓,道長委造,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任均驶,我火速辦了婚禮昏兆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘妇穴。我一直安慰自己爬虱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布腾它。 她就那樣靜靜地躺著跑筝,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瞒滴。 梳的紋絲不亂的頭發(fā)上曲梗,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機(jī)與錄音妓忍,去河邊找鬼虏两。 笑死,一個(gè)胖子當(dāng)著我的面吹牛世剖,可吹牛的內(nèi)容都是我干的定罢。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼旁瘫,長吁一口氣:“原來是場噩夢啊……” “哼祖凫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起酬凳,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤惠况,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后粱年,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體售滤,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了完箩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赐俗。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖弊知,靈堂內(nèi)的尸體忽然破棺而出阻逮,到底是詐尸還是另有隱情,我是刑警寧澤秩彤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布叔扼,位于F島的核電站,受9級特大地震影響漫雷,放射性物質(zhì)發(fā)生泄漏瓜富。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一降盹、第九天 我趴在偏房一處隱蔽的房頂上張望与柑。 院中可真熱鬧,春花似錦蓄坏、人聲如沸价捧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽结蟋。三九已至,卻和暖如春渔彰,著一層夾襖步出監(jiān)牢的瞬間嵌屎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工胳岂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留编整,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓乳丰,卻偏偏與公主長得像掌测,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子产园,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 首先為什么要寫單元測試汞斧? “滿足需求”是所有軟件存在的必要條件,單元測試一定是為它服務(wù)的什燕。從這一點(diǎn)出發(fā)粘勒,我們可以總...
    愛碼小士閱讀 352評論 0 1
  • 我們?yōu)槭裁匆獙憜卧獪y試? 滿足需求是所有軟件存在的必要條件屎即,單元測試一定是為它服務(wù)的庙睡。從這一點(diǎn)出發(fā)事富,我們可以總結(jié)出...
    lambeta閱讀 1,837評論 0 2
  • 在軟件單元測試時(shí),考慮到軟件單元的所有情況有時(shí)是非常困難的乘陪。能不能采取一些有效的辦法统台,能夠在可接受的單元測試用例數(shù)...
    昭陽的母上大人閱讀 4,664評論 0 0
  • 單元測試作為提升代碼質(zhì)量的有效方法,目前在國內(nèi)各大互聯(lián)網(wǎng)公司的開發(fā)團(tuán)隊(duì)中啡邑,尤其是業(yè)務(wù)團(tuán)隊(duì)中卻鮮少被使用贱勃。這主要由于...
    Buynow_Zhao閱讀 1,460評論 0 2
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來的情緒谤逼。表情可以傳達(dá)很多信息贵扰。高興了當(dāng)然就笑了,難過就哭了流部。兩者是相互影響密不可...
    Persistenc_6aea閱讀 124,193評論 2 7