qileilove

          blog已經(jīng)轉(zhuǎn)移至github,大家請(qǐng)?jiān)L問(wèn) http://qaseven.github.io/

          測(cè)試覆蓋(率)到底有什么用?

          引言
            經(jīng)常有人問(wèn)我這樣的問(wèn)題:“我們?cè)谧?a href="" target="_self" style="word-break: break-all; color: #202859; text-decoration: none; line-height: normal !important;">單元測(cè)試,那測(cè)試覆蓋率要到多少才行?”。而我的答案很簡(jiǎn)單,“作為指標(biāo)的測(cè)試覆蓋率都是沒有用處的。”
            Martin Fowler(重構(gòu)那本書的作者)曾經(jīng)寫過(guò)一篇博客來(lái)討論這個(gè)問(wèn)題,他指出:把測(cè)試覆蓋作為質(zhì)量目標(biāo)沒有任何意義,而我們應(yīng)該把它作為一種發(fā)現(xiàn)未被測(cè)試覆蓋的代碼的手段。
            Brian Marick(敏捷宣言最早的17個(gè)簽署人之一)也說(shuō)過(guò),作為一名程序員,我當(dāng)然期望我的代碼有較高的測(cè)試覆蓋率。但是,當(dāng)我的經(jīng)理要求這樣的指標(biāo)時(shí),那就有別的目的了(績(jī)效考核?)。
            我認(rèn)為,高的測(cè)試覆蓋率應(yīng)該是每個(gè)“認(rèn)真”寫單元測(cè)試的程序員得到的必然結(jié)果,管理者把一個(gè)結(jié)果作為指標(biāo)來(lái)衡量,本身就是沒有意義的。如果你把“萬(wàn)能”的程序員逼急了,他就會(huì)從 “神秘的工具箱”中拿出一兩個(gè)“法寶”來(lái),“高效”地達(dá)成指標(biāo)。我就見過(guò)很多這樣的“法寶”,比如在單元測(cè)試中連一個(gè)“assert”也沒有,或者寫很多get和set方法的單元測(cè)試(寫起來(lái)簡(jiǎn)單啊)來(lái)提高整體的覆蓋率等等。更何況,測(cè)試充分的代碼也有可能無(wú)法達(dá)到100%的覆蓋率,本文的后面就有這樣的例子。
            那你大概會(huì)問(wèn):“那測(cè)試覆蓋到底有什么用呢?”。我的答案還是很簡(jiǎn)單,“測(cè)試覆蓋是一種學(xué)習(xí)手段”。學(xué)習(xí)什么呢?學(xué)習(xí)為什么有些代碼沒有被覆蓋到,以及為什么有些代碼變了測(cè)試卻沒有失敗。理解“為什么”背后的原因,程序員就可以做相應(yīng)的改善和提高,相比憑空想象單元測(cè)試的有效性和代碼的好壞,這會(huì)更加有效。
            接下來(lái),我會(huì)給大家介紹一些傳統(tǒng)的測(cè)試覆蓋方法和一種稱為“代碼變異測(cè)試”(Mutation Test)的方法。大家將會(huì)看到這些方法都可以產(chǎn)生什么樣的學(xué)習(xí)點(diǎn),以及代碼變異測(cè)試相比傳統(tǒng)方法更有價(jià)值的地方。如果你是一名程序員(我不會(huì)區(qū)分你是開發(fā)人員還是測(cè)試人員,那對(duì)我來(lái)說(shuō)都一樣),希望你看完這篇文章之后,可以找到一些提高測(cè)試和代碼質(zhì)量的方法。如果你是一位管理者,不論你正在用還是想要用“測(cè)試覆蓋率”來(lái)做度量,希望你看完這篇文章之后,可以放棄這個(gè)想法,做點(diǎn)更有意義的事情(比如去寫點(diǎn)代碼)。
            傳統(tǒng)的測(cè)試覆蓋方法
            傳統(tǒng)的測(cè)試覆蓋方法常見的有以下幾種:
            函數(shù)覆蓋(Function Coverage)
            語(yǔ)句覆蓋(Statement Coverage)
            決策覆蓋(Decision Coverage)
            條件覆蓋(Condition Coverage)
            還有一些其他覆蓋方法,如Modified Condition/Decision Coverage,就不在這里討論了。
            函數(shù)覆蓋:顧名思義,就是指這個(gè)函數(shù)是否被測(cè)試代碼調(diào)用了。以下面的代碼為例,對(duì)函數(shù)foo要做到覆蓋,只要一個(gè)測(cè)試——如assertEquals(2, foo(2, 2))——就可以了。如果連函數(shù)覆蓋都達(dá)不到,那應(yīng)該想想這個(gè)函數(shù)是否真的需要了。如果需要的話,那又為什么寫不了一個(gè)測(cè)試呢?
            語(yǔ)句覆蓋:(也稱行覆蓋),指的是某一行代碼是否被測(cè)試覆蓋了。同樣的代碼要達(dá)到語(yǔ)句覆蓋也只需要一個(gè)測(cè)試就夠了,如assertEquals(2, foo(2, 2))。但是,如果把測(cè)試換成assertEquals(0, foo(2, -1)),那就無(wú)法達(dá)到所有行覆蓋的效果了。通常這種情況是由于一些分支語(yǔ)句導(dǎo)致的,因?yàn)橄鄳?yīng)的問(wèn)題就是“那行代碼(以及它所對(duì)應(yīng)的分支)需要嗎?”,或者“用什么測(cè)試可以覆蓋那行代碼所代表的分支呢?”。


          決策覆蓋:指的是某一個(gè)邏輯分支是否被測(cè)試覆蓋了。如我上面所說(shuō),語(yǔ)句覆蓋通常和決策覆蓋有關(guān)系。還是以上面的代碼為例,要達(dá)到所有的決策覆蓋(即那個(gè)if語(yǔ)句為真和假的情況至少出現(xiàn)一次),我們需要至少兩個(gè)測(cè)試,如assertEquals(2, foo(2, 2))和assertEquals(0, foo(-1, 2))。如果有一個(gè)邏輯分支沒有被覆蓋(比如只有測(cè)試assertEquals(2, foo(2, 2))),那么我們應(yīng)該問(wèn)和上面“語(yǔ)句覆蓋”小節(jié)中相似的問(wèn)題。
            條件覆蓋:指的是分支中的每個(gè)條件(即與,或,非邏輯運(yùn)算中的每一個(gè)條件判斷)是否被測(cè)試覆蓋了。之前的代碼要達(dá)到全部的條件覆蓋(也就是x>0和y>0這兩個(gè)條件為真和假的情況均至少出現(xiàn)一次)需要更多的測(cè)試,如assertEquals(2, foo(2, 2)),assertEquals(2, foo(2, -1))和assertEquals(2, foo(-1, -1))。如果有一個(gè)條件分支沒有被覆蓋(比如缺少測(cè)試assertEquals(2, foo(-1, -1))),那么大家應(yīng)該想想“那個(gè)條件判斷是否還需要呢?”,或者“用什么測(cè)試可以覆蓋那個(gè)條件所對(duì)應(yīng)的邏輯呢?”。
            通過(guò)上面對(duì)幾種傳統(tǒng)的測(cè)試覆蓋方法的介紹,大家不難發(fā)現(xiàn),這些方法的確可以幫我們找到一些顯而易見的代碼冗余或者測(cè)試遺漏的問(wèn)題。不過(guò),實(shí)踐證明,這些傳統(tǒng)的方法只能產(chǎn)生非常有限的“學(xué)習(xí)”代碼和測(cè)試中問(wèn)題的機(jī)會(huì)。很多代碼和測(cè)試的問(wèn)題即使在達(dá)到100%覆蓋的情況下也無(wú)法發(fā)現(xiàn)。然而,我接下來(lái)要介紹的“代碼變異測(cè)試”這種方法則,它可以很好的彌補(bǔ)傳統(tǒng)方法的缺點(diǎn),產(chǎn)生更加有效的“學(xué)習(xí)”機(jī)會(huì)。
            代碼變異測(cè)試(Mutation Test)
            代碼變異測(cè)試是通過(guò)對(duì)代碼產(chǎn)生“變異”來(lái)幫助我們學(xué)習(xí)的。“變異”指的是修改一處代碼來(lái)改變代碼行為(當(dāng)然保證語(yǔ)法的合理性)。簡(jiǎn)單來(lái)說(shuō),代碼變異測(cè)試先試著對(duì)代碼產(chǎn)生這樣的變異,然后運(yùn)行單元測(cè)試,并檢查是否有任何測(cè)試因?yàn)檫@個(gè)代碼變異而失敗。如果有測(cè)試失敗,那么說(shuō)明這個(gè)變異被“消滅”了,這是我們期望看到的結(jié)果。如果沒有測(cè)試失敗,則說(shuō)明這個(gè)變異“存活”了下來(lái),這種情況下我們就需要去研究一下“為什么”了。
            是不是感覺有點(diǎn)繞呢?讓我們換個(gè)角度來(lái)說(shuō)明一下,可能就容易理解了。測(cè)試驅(qū)動(dòng)開發(fā)相信大家一定都聽說(shuō)過(guò),它的一個(gè)重要觀點(diǎn)是,我們應(yīng)該以最簡(jiǎn)單的代碼來(lái)通過(guò)測(cè)試(剛好夠,Just Enough)。基于這個(gè)前提,那么幾乎所有的代碼修改(即“變異”)都應(yīng)該會(huì)改變代碼的行為,從而導(dǎo)致測(cè)試失敗。這樣的話,如果有個(gè)變異沒有導(dǎo)致測(cè)試失敗,那要么是代碼有冗余,要么就是測(cè)試不足以發(fā)現(xiàn)這個(gè)變異。
            另一方面,大家可以想一下對(duì)于自動(dòng)化測(cè)試(包括單元測(cè)試)的期望是什么。我覺得一個(gè)很重要的期望就是,自動(dòng)化測(cè)試可以防止“任何”錯(cuò)誤的代碼修改,以減少代碼維護(hù)帶來(lái)的風(fēng)險(xiǎn)。錯(cuò)誤的代碼修改實(shí)際上就是一個(gè)代碼變異,代碼變異測(cè)試可以幫我們找到一些無(wú)法被當(dāng)前測(cè)試所防止的潛在錯(cuò)誤。
            舉例來(lái)說(shuō),我們給之前的那段被測(cè)代碼增加一行,sideEffect(z)。之前的那些可以讓傳統(tǒng)的測(cè)試覆蓋方法達(dá)到100%覆蓋率的測(cè)試,在新增這行代碼之后,依然會(huì)全部通過(guò)且覆蓋率不變。然而,如果我們?cè)賱h除那行新代碼sideEffect(z),結(jié)果有會(huì)怎樣呢?那些測(cè)試還是會(huì)全部通過(guò),覆蓋率也還是100%。在這種情況下,原來(lái)那些測(cè)試可以說(shuō)沒有任何意義。相對(duì)的,代碼變異測(cè)試則可以通過(guò)刪除那一行,再運(yùn)行測(cè)試,就會(huì)發(fā)現(xiàn)沒有任何測(cè)試失敗。然后,我們就可以根據(jù)這個(gè)結(jié)果想到其實(shí)還需要一個(gè)測(cè)試來(lái)驗(yàn)證sideEffect(z)這個(gè)行為(如果那行代碼不是多余的話)。
            再舉一個(gè)例子,還是之前的代碼,不做任何修改。我們用assertEquals(2, foo(2, 2)),assertEquals(2, foo(2, -1))和assertEquals(2, foo(-1, -1))這三個(gè)測(cè)試達(dá)到了100%的條件覆蓋。然而,如果把y > 0的條件改成 y >= 0的話,這三個(gè)測(cè)試依然會(huì)通過(guò)。為什么會(huì)出現(xiàn)這樣的問(wèn)題呢?那是因?yàn)橹暗臏y(cè)試對(duì)輸入?yún)?shù)的選擇比較隨意,所以讓這個(gè)代碼變異存活了下來(lái)。可以看到,在條件覆蓋100%的情況下,代碼變異測(cè)試依然可以幫我們發(fā)現(xiàn)這種測(cè)試寫的不嚴(yán)謹(jǐn)?shù)膯?wèn)題(假設(shè)y >= 0這個(gè)代碼變異是不合理的),從而使修改后的測(cè)試可以防止產(chǎn)生這樣的錯(cuò)誤代碼。
            通過(guò)上面兩個(gè)例子,相信大家已經(jīng)發(fā)現(xiàn)代碼變異測(cè)試可以給我們提供大量的學(xué)習(xí)代碼合理性和測(cè)試有效性的機(jī)會(huì)。實(shí)際上,類似的代碼變異還有很多種。下面是常見變異的列表,更詳細(xì)的內(nèi)容可以參考http://pitest.org/quickstart/mutators/。
          條件邊界變異(Conditionals Boundary Mutator)
            對(duì)關(guān)系運(yùn)算(<, <=, >, >=)進(jìn)行變異,上面第二例子就是這種變異
            反向條件變異(Negate Conditionals Mutator)
            對(duì)關(guān)系運(yùn)算(==, !=, <, <=, >, >=)進(jìn)行變異,例如把“==”變成“!=”
            數(shù)學(xué)運(yùn)算變異(Math Mutator)
            對(duì)數(shù)學(xué)運(yùn)算(+, -, *, /, %, &, |, ^, >>, <<, >>>)進(jìn)行變異,例如把“+”變成“-”
            增量運(yùn)算變異(Increments Mutator)
            對(duì)遞增或者遞減的運(yùn)算(++, --)進(jìn)行變異,例如把“++”變成“--”
            負(fù)值翻轉(zhuǎn)變異(Invert Negatives Mutator)
            對(duì)負(fù)數(shù)表示的變量進(jìn)行變異,例如把“return -i”變成“return i”
            內(nèi)聯(lián)常量變異(Inline Constant Mutator)
            對(duì)代碼中用到的常量數(shù)字進(jìn)行變異,例如把“int i=42”變成“int i=43”
            返回值變異(Return Values Mutator)
            對(duì)代碼中的返回值進(jìn)行變異,例如把“return 0”變成“return 1”或者把“return new Object();”變成“new Object(); return null;”
            無(wú)返回值方法調(diào)用變異(Void Method Calls Mutator)
            對(duì)代碼中的無(wú)返回值方法調(diào)用進(jìn)行變異,也就是把那個(gè)方法調(diào)用刪除掉,上面的第一個(gè)例子就是這種變異。
            有返回值方法調(diào)用變異(Non Void Method Calls Mutator)
            對(duì)代碼中的有返回值函數(shù)調(diào)用進(jìn)行變異,也就是接收返回值的變量賦值將被替換成為返回值類型的語(yǔ)言默認(rèn)值,例如把“int i = getSomeIntValue()”變成“int i = 0”
            構(gòu)造函數(shù)調(diào)用變異(Constructor Calls Mutator)
            對(duì)代碼中的構(gòu)造函數(shù)調(diào)用進(jìn)行變異,例如把“Object o = new Object()”變成“Object o == null”
            測(cè)試驅(qū)動(dòng)開發(fā)和代碼變異測(cè)試
            測(cè)試驅(qū)動(dòng)開發(fā)(TDD)是我推崇和實(shí)踐的寫代碼(做設(shè)計(jì))方法。我在前面曾經(jīng)提到,代碼變異測(cè)試的假設(shè)是“實(shí)現(xiàn)代碼是剛好夠通過(guò)測(cè)試的最簡(jiǎn)單代碼”,而這也是TDD中的重要實(shí)踐之一。大家可能會(huì)問(wèn),如果做了TDD,代碼變異測(cè)試的結(jié)果又會(huì)如何呢?還會(huì)產(chǎn)生學(xué)習(xí)的機(jī)會(huì)嗎?答案是肯定的,一定會(huì)。
            讓我們通過(guò)例子來(lái)看一下。我經(jīng)常會(huì)做一些Kata來(lái)練習(xí)編程技巧,PokerHands(如上圖)就是其中之一(其實(shí)大體就是實(shí)現(xiàn)梭哈的五張比較規(guī)則http://codingdojo.org/cgi-bin/wiki.pl?KataPokerHands)。每次我把Kata做完之后,都會(huì)用運(yùn)行一下代碼變異測(cè)試(sonar中有插件)。Java的代碼變異測(cè)試工具有個(gè)比較好的叫pitest。下面是我用這個(gè)工具跑出來(lái)的結(jié)果,代碼可以在這里找到https://github.com/JosephYao/Kata-PokerHands。
            如大家所見,紅色那一行中有一個(gè)存活下來(lái)的代碼變異。而這個(gè)代碼變異是把“index < CARD_COUNT - 1”中的“<”換成“>”。看上去很不可思議吧,因?yàn)檫M(jìn)行這樣的代碼變異意味著整個(gè)for循環(huán)都不會(huì)被執(zhí)行了,應(yīng)該不可能沒有一個(gè)測(cè)試失敗吧?
            讓我們來(lái)看一下相關(guān)的單元測(cè)試。在下面這個(gè)測(cè)試中有三個(gè)assert,它們都是在驗(yàn)證“一對(duì)”之間通過(guò)對(duì)子的點(diǎn)數(shù)來(lái)比較大小的情況。大家仔細(xì)觀察就可以發(fā)現(xiàn),其實(shí)這三個(gè)assert中的牌如果作為High Card(就是比一對(duì)小一點(diǎn)的牌組)來(lái)比較的話,也都是成立的。這也就是那個(gè)代碼變異可以存活下來(lái)的原因,因?yàn)榧词购雎粤艘粚?duì)之間的比較,通過(guò)High Card比較出來(lái)的大小關(guān)系也是一樣的。我從中學(xué)到的是,只要把 assertPokerHandsLargerThan("2S 3H 5S 8C 8D","2S 3H 5S 7C 7D")改為 assertPokerHandsLargerThan("2S 3H 5S 8C 8D","2S 3H 9S 7C 7D")就可以清除這個(gè)代碼變異了。
            從這個(gè)例子中可以看到,即使以TDD的方法來(lái)寫代碼,也是無(wú)法完全避免出現(xiàn)代碼變異存活下來(lái)的情況的(當(dāng)然,存活變異的數(shù)量要非常明顯的少于不用TDD而寫出來(lái)的代碼)。做過(guò)TDD的人可能都有這樣的感覺,就是有時(shí)很難抑制自己寫出復(fù)雜代碼的沖動(dòng)(也就是說(shuō)代碼不是“剛好夠”的)。有時(shí),即使實(shí)現(xiàn)代碼是最簡(jiǎn)單的,也可能因?yàn)榇a過(guò)于直接,就會(huì)很“隨意”的寫出一個(gè)讓當(dāng)前代碼失敗的測(cè)試。上面的例子就是這種情況,這樣不太“有效”的測(cè)試通常在TDD過(guò)程中很難意識(shí)到,從而給之后的代碼維護(hù)造成隱患。
            除了上面那個(gè)有學(xué)習(xí)意義的代碼變異之外,其實(shí)工具還幫我找到了一個(gè)“沒意義”但存活下來(lái)的代碼變異。
            這里存活下來(lái)的代碼變異是指把“index < CARD_COUNT - 2”中的“<”變成“<=”。之所以說(shuō)這個(gè)代碼變異沒意義,是因?yàn)楦鶕?jù)代碼上下文,在for循環(huán)中一定會(huì)在index等于CARD_COUNT - 2之前就找到那個(gè)三張的點(diǎn)數(shù)。因?yàn)楣ぞ邿o(wú)法理解上下文,所以產(chǎn)生了這個(gè)沒意義的代碼變異(也叫做Equivalent Mutation)。之所以舉這個(gè)例子,只是想提醒大家不要迷信代碼變異測(cè)試工具。對(duì)于他產(chǎn)生的結(jié)果一定去分析和學(xué)習(xí),不然很容易走上考核指標(biāo)的那條不歸路。
            小結(jié)
            總而言之,測(cè)試覆蓋這種方法是一種不錯(cuò)的學(xué)習(xí)手段,可以幫助我們提高代碼和測(cè)試質(zhì)量。代碼變異測(cè)試則比傳統(tǒng)的測(cè)試覆蓋方法可以更加有效的發(fā)現(xiàn)代碼和測(cè)試中潛在的問(wèn)題,提供更多的學(xué)習(xí)機(jī)會(huì)。在這里,我要鄭重警告那些妄圖把代碼變異測(cè)試變成一種新的考核指標(biāo)的管理者們,這樣做只會(huì)迫使程序員從他的神秘工具箱中找出新的法寶來(lái)對(duì)付你(比如,修改編譯器等等)。
            代碼變異測(cè)試的概念其實(shí)早在30年前就被提出了。之所以到目前為止還沒有被業(yè)界廣泛接納,一個(gè)重要原因是由于需要對(duì)每個(gè)代碼變異反復(fù)運(yùn)行測(cè)試。如果不是單元測(cè)試(運(yùn)行速度慢),代碼變異測(cè)試工具執(zhí)行時(shí)將消耗大量的時(shí)間。正因如此,單元測(cè)試可能是唯一符合代碼變異測(cè)試要求的一種測(cè)試了。如果你對(duì)代碼變異測(cè)試的歷史和發(fā)展過(guò)程感興趣的話,你可以參考這篇研究報(bào)告http://crestweb.cs.ucl.ac.uk/resources/mutation_testing_repository/TR-09-06.pdf。

          posted on 2014-01-29 10:47 順其自然EVO 閱讀(428) 評(píng)論(1)  編輯  收藏 所屬分類: 測(cè)試學(xué)習(xí)專欄

          評(píng)論

          # re: 測(cè)試覆蓋(率)到底有什么用?[未登錄] 2014-01-29 17:10 海邊沫沫

          學(xué)習(xí)了。
          這么好的文章為什么不放在首頁(yè)上呢?  回復(fù)  更多評(píng)論   

          <2014年1月>
          2930311234
          567891011
          12131415161718
          19202122232425
          2627282930311
          2345678

          導(dǎo)航

          統(tǒng)計(jì)

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評(píng)論

          閱讀排行榜

          評(píng)論排行榜

          主站蜘蛛池模板: 嘉祥县| 纳雍县| 晋江市| 绍兴县| 即墨市| 和政县| 巴里| 隆子县| 改则县| 罗甸县| 上杭县| 江阴市| 察隅县| 恭城| 贵港市| 衡山县| 深州市| 珠海市| 西畴县| 阿巴嘎旗| 巫溪县| 甘谷县| 醴陵市| 太和县| 安顺市| 石棉县| 迭部县| 蓝田县| 蓝山县| 吉林省| 宽城| 萨嘎县| 邵阳市| 怀来县| 金塔县| 平顺县| 天等县| 进贤县| 扶沟县| 谢通门县| 上虞市|