麻省理工18年春软件构造课程阅读03“测试”
本文內(nèi)容來自MIT_6.031_sp18: Software Construction課程的Readings部分,采用CC BY-SA 4.0協(xié)議。
由于我們學(xué)校(哈工大)大二軟件構(gòu)造課程的大部分素材取自此,也是推薦的閱讀材料之一,于是打算做一些翻譯工作,自己學(xué)習(xí)的同時(shí)也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有許多練習(xí)題,但是沒有標(biāo)準(zhǔn)答案,所給出的答案均為譯者所寫,有錯(cuò)誤的地方還請指出。
譯者:李秋豪 江家偉
審校:李秋豪
V1.0 Tue Mar 6 01:54:34 CST 2018
本次課程的目標(biāo)
- 理解測試的意義,并了解“測試優(yōu)先編程”的過程
- 能夠使用“分區(qū)”的方法選擇合適的輸入輸出測試用例
- 能夠通過代碼覆蓋率來評價(jià)一個(gè)測試的好壞
- 理解黑盒/白盒測試、單元/集成測試、自動(dòng)化回歸測試。
驗(yàn)證(Validation)
“測試”是“驗(yàn)證”的一種例子。而驗(yàn)證的目的就是發(fā)現(xiàn)程序中的問題,以此提升你對程序正確性的信心。驗(yàn)證包括:
- 形式推理,即通過理論推理證明程序的正確性。形式推理目前還缺乏自動(dòng)化的工具,通常需要漫長的手工計(jì)算。即使是這樣,一些關(guān)鍵性的小程序也是需要被證明的,例如操作系統(tǒng)的調(diào)度程序、虛擬機(jī)里的字節(jié)碼解釋器,或者是 文件系統(tǒng).
- 代碼審查. 即讓別人仔細(xì)的閱讀、審校、評價(jià)你的代碼,這也是發(fā)現(xiàn)bug的一個(gè)常用方法,我們會(huì)在下一個(gè)reading里面介紹這種方法。
- 測試.即選擇合適的輸入輸出用例,通過運(yùn)行程序檢查程序的問題。
即使是最優(yōu)的驗(yàn)證,程序也不可能達(dá)到十全十美,這里列出了一般情況下程序的剩余缺陷率residual defect rates(軟件發(fā)行時(shí)存在的bug比率) ,這里的單位是每 kloc (每一千行代碼):
- 1 - 10 defects/kloc: 常見的工業(yè)級軟件。
- 0.1 - 1 defects/kloc: 高質(zhì)量驗(yàn)證后的軟件。例如Java的官方庫可能就是這個(gè)級別。
- 0.01 - 0.1 defects/kloc: 最高級別、軍工/安全關(guān)鍵軟件。例如NASA或者像Praxis這樣的公司(譯者注:1.歐洲著名的電力行業(yè)信息化解決方案專家,隸屬于世界第三大的電力集團(tuán)E.ON,總部位于德國。2.美國教師資格證考試 這里不知道說的是第一個(gè)還是第二個(gè)(體現(xiàn)一下幽默感?))
這看起來讓人沮喪,想一想,如果你寫了100萬行的大型程序,那你很可能沒檢查出1000個(gè)bug!
為什么軟件測試很困難
這里有一些在工業(yè)界測試產(chǎn)品常用的方法,可是它們在軟件行業(yè)無法發(fā)揮應(yīng)有的作用。
盡力測試(嘗試所有的可能):這通常是不可行的,因?yàn)榇蠖鄶?shù)情況下輸入空間會(huì)非常大,例如僅僅是一個(gè)浮點(diǎn)數(shù)乘法a*b ,其總共的取值就有2^64種可能性!
隨機(jī)測試 (試一下看看行不行): 這通常難以發(fā)現(xiàn)bug,除非這個(gè)程序到處都是bug以至于隨便一個(gè)輸入都能崩潰。即使我們修復(fù)了測試出來的bug,隨機(jī)的輸入也不能使我們對程序的正確性很確定。
基于統(tǒng)計(jì)方法的測試:遺憾的是,這種方法對軟件不那么奏效。在物理系統(tǒng)里,工程師可以通過特定的方法加速實(shí)驗(yàn)的進(jìn)程,例如在一天的時(shí)間里打開關(guān)閉一個(gè)冰箱門一千次,以此來模擬幾年的正常使用,最終得到產(chǎn)品的”失敗率“。以后的測試結(jié)果也將會(huì)集中分布在這個(gè)比率左右,工程師們就對這個(gè)比率進(jìn)行進(jìn)一步的研究。但是軟件的行為通常是離散且不可預(yù)測的。程序可能在上一秒還完全正常的工作,突然就崩潰了,也可能對于大多數(shù)輸入都沒問題,對于一個(gè)值就崩潰了(沒有預(yù)兆,更談不上失敗率,所以很難提前做好監(jiān)測的準(zhǔn)備),例如 有名的奔騰處理器除法bug ,在90億次的除法中才可能會(huì)有一個(gè)錯(cuò)誤。
綜上,我們必須系統(tǒng)而且小心的選擇測試用例,這也是下面要講的。
閱讀小練習(xí)
測試基礎(chǔ)
阿麗亞娜5型火箭,為歐洲空間局研發(fā)的民用衛(wèi)星一次性運(yùn)載火箭,名稱來源于神話人物阿麗雅杜妮(Ariadne)的法語拼寫。1996年6月4日,在風(fēng)和日麗的法屬圭亞那太空發(fā)射場,阿麗亞娜5型運(yùn)載火箭首航,計(jì)劃運(yùn)送4顆太陽風(fēng)觀察衛(wèi)星到預(yù)定軌道。但在點(diǎn)火升空之后的40秒后,在4000米高空,這個(gè)價(jià)值5億美元的運(yùn)載系統(tǒng)就發(fā)生了爆炸,瞬間灰飛煙滅化為烏有。
爆炸原因由于火箭某段控制程序直接移植自阿麗亞娜4型火箭,其中一個(gè)需要接收64位數(shù)據(jù)的變量為了節(jié)省存儲(chǔ)空間而使用了16位字節(jié),從而在控制過程中產(chǎn)生了整數(shù)溢出,導(dǎo)致導(dǎo)航系統(tǒng)對火箭控制失效,程序進(jìn)入異常處理模塊,引爆自毀。
這個(gè)故事告訴了我們什么?
- [x] 即使是高度關(guān)鍵性的程序也可能有bug
- [ ] 測試所有可能輸入是解決這樣的問題的最好辦法
- [x] 與很多物理工程學(xué)上的系統(tǒng)不同,軟件的行為是離散的
- [ ] 靜態(tài)檢查有助于發(fā)現(xiàn)這個(gè)bug
靜態(tài)類型檢查不會(huì)檢測到此錯(cuò)誤,因?yàn)榇a有意(強(qiáng)轉(zhuǎn))將64位精度轉(zhuǎn)換為16位精度。
測試應(yīng)該具備的心態(tài)(Putting on Your Testing Hat)
測試需要一個(gè)正確的態(tài)度:當(dāng)你在寫一個(gè)程序的時(shí)候,你的心態(tài)一定是讓這個(gè)程序正常運(yùn)行,但是作為一個(gè)測試者,你應(yīng)該想方設(shè)法讓程序崩潰。
這是一個(gè)隱晦但重要的區(qū)別,一個(gè)優(yōu)秀的測試員會(huì)“揮舞的重錘敲打代碼可能有問題的地方”,而不是“小心的呵護(hù)它”。
測試優(yōu)先編程(Test-first Programming)
測試開始的時(shí)間應(yīng)該盡量早,并且要頻繁地測試。當(dāng)你有一大堆未經(jīng)驗(yàn)證的代碼時(shí),不要把測試工作留到最后。把測試工作留到最后只會(huì)讓調(diào)試的時(shí)間更久并且調(diào)試過程更加痛苦,因?yàn)槟愕拇a將會(huì)充斥著bug。反之,如果你在編碼的過程中就進(jìn)行測試,情況就會(huì)好的多。
在測試優(yōu)先編程中,測試程序先于代碼完成。編寫一個(gè)函數(shù)應(yīng)該按如下步驟進(jìn)行:
規(guī)格說明描述了這個(gè)函數(shù)的輸入輸出行為。它確定了函數(shù)參數(shù)的類型和對它們的所有約束(例如sqrt函數(shù)的參數(shù)必須是非負(fù)的)。它還定義了函數(shù)的返回值類型以及返回值和輸入之間的關(guān)系。你已經(jīng)在這門課中對許多問題都查看并使用過規(guī)格說明。在代碼中,規(guī)格說明包括了函數(shù)簽名和一些描述函數(shù)功能的注釋。我們將會(huì)在接下來的幾節(jié)課里討論更多關(guān)于規(guī)格說明的問題。
先完成測試用例的編寫能夠讓你更好地理解規(guī)格說明。規(guī)格說明也可能存在問題——不正確、不完整、模棱兩可、缺失邊界情況。先嘗試編寫測試用例,可以在你浪費(fèi)時(shí)間實(shí)現(xiàn)一個(gè)有問題的規(guī)格說明之前發(fā)現(xiàn)這些問題。
通過分區(qū)的方法選擇測試用例
選擇合適的測試用例是一個(gè)具有挑戰(zhàn)性但是有缺的問題。我們即希望測試空間足夠小,以便能夠快速完成測試,又希望測試用例能夠驗(yàn)證盡可能多的情況。
為了達(dá)到這個(gè)目的,我們可以先將輸入空間劃分為幾個(gè)子域(subdomains) ,每一個(gè)子域都是一類相似的數(shù)據(jù)。如上圖所示,我們在每個(gè)子域中選取一些數(shù)據(jù),它們合并起來就是我們需要的輸入用例。
分區(qū)背后的原理在于同一類型的數(shù)據(jù)在程序中的行為大多類似,所以我們可以用一小部分代表整體的行為。這個(gè)方法的優(yōu)點(diǎn)在于強(qiáng)迫程序相應(yīng)輸入空間里的不同地方,有效的利用了測試資源。
如果我們要確保測試的輸出能夠覆蓋輸出空間的不同地方,也可以將輸出空間劃分為幾個(gè)子域(哪些輸出代表程序發(fā)生了相似的行為)。大多數(shù)情況下,對輸入分區(qū)就足夠了
例子1: BigInteger.multiply()
現(xiàn)在讓我們來看一個(gè)例子。 BigInteger 是Java庫中的一個(gè)類,它能夠表示任意大小的整數(shù)。同時(shí),它有一個(gè)multiply 方法,能夠?qū)蓚€(gè)BigInteger類型的值進(jìn)行相乘操作:
/*** @param val another BigInteger* @return a BigInteger whose value is (this * val).*/ public BigInteger multiply(BigInteger val)例如,計(jì)算ab的值:
BigInteger a = ...; BigInteger b = ...; BigInteger ab = a.multiply(b);這個(gè)例子顯示即使只有一個(gè)參數(shù),這個(gè)操作實(shí)際上有兩個(gè)操作符:你調(diào)用這個(gè)方法所在的對象(上面是a ),以及你傳入的參數(shù)(上面是b )。(在Python中,接受方法調(diào)用的對象會(huì)顯式以self被聲明。在Java中你不需要聲明這個(gè)對象,它隱式的被稱作this )我們可以把 multiply 看成一個(gè)有兩個(gè)參數(shù)的方法,參數(shù)的類型是 BigInteger ,并且輸出的類型也是 BigInteger 即:
multiply : BigInteger × BigInteger → BigInteger
所以我們的輸入空間是二維的,用二維點(diǎn)陣(a,b)表示?,F(xiàn)在我們對其進(jìn)行分區(qū),想一想乘法是怎么工作的,我們可以將點(diǎn)陣初步分為以下四個(gè)區(qū):
- a和b都是正整數(shù)
- a和b都是負(fù)整數(shù)
- a是正整數(shù),b是負(fù)整數(shù)
- b是正整數(shù),a是負(fù)整數(shù)
這里也有一些特殊的情況要單獨(dú)分出來:0 1 -1
- a或b是1\0\-1
最后,作為一個(gè)認(rèn)真的測試員,我們還要想一想BigInteger的乘法可能是怎么運(yùn)算的:它可能在輸入數(shù)據(jù)絕對值較小時(shí)使用 int 或 long ,這樣運(yùn)算起來快一些,只有當(dāng)數(shù)據(jù)很大時(shí)才會(huì)使用更費(fèi)勁的存儲(chǔ)方法(例如列表)。所以我們也應(yīng)該將對數(shù)據(jù)的大小進(jìn)行分區(qū):
- a或b較小
- a或b的絕對值大于Long.MAX_VALUE ,即Java原始整型的最大值,大約是2^63。
現(xiàn)在我們可以將上面劃分的區(qū)域整合起來,得到最終劃分的點(diǎn)陣:
- 0
- 1
- -1
- 較小正整數(shù)
- 較小負(fù)整數(shù)
- 大正整數(shù)
- 大負(fù)整數(shù)
所以我們一共可以得到 7 × 7 = 49 個(gè)分區(qū),它們完全覆蓋了a和b組成的所有輸入空間。然后從這個(gè)”柵欄“里的每個(gè)區(qū)選取各自的測試用例,例如:
- (a,b) = (-3, 25) 代表 (小負(fù)整數(shù), 小正整數(shù))
- (a,b) = (0, 30) 代表 (0, 小正整數(shù))
- (a,b) = (2^100, 1) 代表 (大正整數(shù), 1)
- 等等
例子2: max()
現(xiàn)在我們看看Java庫中的另一個(gè)例子:針對整數(shù)int的max() 函數(shù),它屬于 Math 類:
/*** @param a an argument* @param b another argument* @return the larger of a and b.*/ public static int max(int a, int b)和上面的例子一樣,我們先分析輸入空間:
max : int × int → int (譯者注:這里的乘號不代表乘法,而是一種封閉的二元運(yùn)算關(guān)系,參見近世代數(shù))
通過描述分析,我們可以將其分區(qū)為:
- a < b
- a = b
- a > b
所以可以選擇以下測試用例:
- (a, b) = (1, 2) 代表 a < b
- (a, b) = (9, 9) 代表 a = b
- (a, b) = (-5, -6) 代表 a > b
注意分區(qū)之間的“邊界”
bug經(jīng)常會(huì)在各個(gè)分區(qū)的邊界處發(fā)生,例如:
- 在正整數(shù)和負(fù)整數(shù)之間的0
- 數(shù)字類型的最大值和最小值,例如 int 和 double
- 空集,例如空的字符串,空的列表,空的數(shù)組
- 集合類型中的第一個(gè)元素或最后一個(gè)元素
為什么這些邊界的地方經(jīng)常產(chǎn)生bug呢?一個(gè)很重要的原因就是程序員經(jīng)常犯“丟失一個(gè)(off-by-one mistakes)”的錯(cuò)誤。例如將<=寫成< ,或者將計(jì)數(shù)器用0來初始化而不是1。另外一個(gè)原因就是邊界處的值可能需要用特殊的行為來處理,例如當(dāng)int類型的變量達(dá)到最大值以后,再對其加正整數(shù)反而會(huì)變成負(fù)數(shù)。
所以,我們在分區(qū)后,測試用例不要忘了加上邊界上的值,現(xiàn)在重新做一下上面那個(gè)例子:
max : int × int → int.
分區(qū):
- a與b的關(guān)系
- a < b
- a = b
- a > b
- a的值
- a = 0
- a < 0
- a > 0
- a = 最小的整數(shù)
- a = 最大的整數(shù)
- value of b
- b = 0
- b < 0
- b > 0
- b = 最小的整數(shù)
- b = 最大的整數(shù)
現(xiàn)在我們再次選取測試用例覆蓋上面的分區(qū)和邊界值:
- (1, 2) 代表 a < b, a > 0, b > 0
- (-1, -3) 代表 a > b, a < 0, b < 0
- (0, 0) 代表 a = b, a = 0, b = 0
- (Integer.MIN_VALUE, Integer.MAX_VALUE) 代表 a < b, a = minint, b = maxint
- (Integer.MAX_VALUE, Integer.MIN_VALUE) 代表 a > b, a = maxint, b = minint
覆蓋分區(qū)的兩個(gè)極限情況
在分區(qū)后,我們可以選擇“盡力(how exhaustive we want)”的程度來測試我們的分區(qū),這里有兩個(gè)極限情況:
- 完全笛卡爾乘積
即對每一個(gè)存在組合都進(jìn)行測試。例如在第一個(gè)例子multiply中,我們一共使用了 7 × 7 = 49 個(gè)測試用例,每一個(gè)組合都用上了。對于第二個(gè)例子,就會(huì)是 3 × 5 × 5 = 75個(gè)測試用例。要注意的是,實(shí)際上有一些組合是不存在的,例如 a < b, a=0, b=0。 - 每一個(gè)分區(qū)被覆蓋即可
即每一個(gè)分區(qū)至少被覆蓋一次。例如我們在第二個(gè)例子max中只使用了5個(gè)測試用例,但是這5個(gè)用例覆蓋到了我們的三維輸入空間的所有分區(qū)。
在實(shí)際測試中我們通常在這兩個(gè)極限中折中,這種折中是基于人們的經(jīng)驗(yàn),對代碼的獲取度(黑白盒測試)、以及對代碼的覆蓋率,這些我們會(huì)在后面講到。
閱讀小練習(xí)
分區(qū)
思考下面這個(gè)規(guī)格說明:
/*** Reverses the end of a string.** 012345 012345* For example: reverseEnd("Hello, world", 5) returns "Hellodlrow ,"* <-----> <----->** With start == 0, reverses the entire text.* With start == text.length(), reverses nothing.** @param text non-null String that will have its end reversed* @param start the index at which the remainder of the input is reversed,* requires 0 <= start <= text.length()* @return input text with the substring from start to the end of the string reversed*/ public static String reverseEnd(String text, int start)對于 start 參數(shù)進(jìn)行測試,下面的哪一個(gè)分區(qū)是合理的 ?
- [ ] start = 0, start = 5, start = 100
- [ ] start < 0, start = 0, start > 0
- [x] start = 0, 0 < start < text.length(), start = text.length()
- [ ] start < text.length(), start = text.length(), start > text.length()
譯者注:要特別注意的是,本文談到的都是對程序正確性進(jìn)行測試,即輸入都是規(guī)格說明里面的合法值。至于那些非法的值則是對魯棒性(robust)或者安全性的測試。
對于 text 參數(shù)進(jìn)行測試,下面的哪一個(gè)分區(qū)是合理的 ?
- [ ] text 包含一些數(shù)字; text不包含字母, 但是包含一些數(shù)字; text 既不包含字母,也不包含數(shù)字
- [ ] text.length() = 0; text.length() > 0
- [x] text.length() = 0; text.length()-start 是奇數(shù); text.length()-start 是偶數(shù)(譯者注,這個(gè)選項(xiàng)是第二個(gè)的超集,多的地方在于奇數(shù)偶數(shù)的判斷,原因在于如果一個(gè)字符串字符的個(gè)數(shù)是奇數(shù)個(gè),那么中間的那個(gè)字符就不需要移動(dòng)位置了,這可能需要特殊的行為來處理,也可能是bug產(chǎn)生的原因)
- [ ] 測試0到100個(gè)字符的所有字符串
用JUnit做自動(dòng)化單元測試
一個(gè)良好的測試程序應(yīng)該測試軟件的每一個(gè)模塊(方法或者類)。如果這種測試每次是對一個(gè)孤立的模塊單獨(dú)進(jìn)行的,那么這就稱為“單元測試”。單元測試的好處在于debug,如果你發(fā)現(xiàn)一個(gè)單元測試失敗了,那么bug很可能就在這個(gè)單元內(nèi)部,而不是軟件的其他地方。
JUnit 是Java中一個(gè)被廣泛只用的測試庫,我們在以后的課程中也會(huì)大量使用它。一個(gè)JUnit測試單元是以一個(gè)方法(method)寫出的,其首部有一個(gè) @Test聲明。一個(gè)測試單元通常含有對測試的模塊進(jìn)行的一次或多次調(diào)用,同時(shí)會(huì)用斷言檢查模塊的返回值,比如 assertEquals, assertTrue, 和 assertFalse.i
例如,我們對上面提到的 Math.max() 模塊進(jìn)行測試,JUnit就可以這樣寫:
@Test public void testALessThanB() {assertEquals(2, Math.max(1, 2)); }@Test public void testBothEqual() {assertEquals(9, Math.max(9, 9)); }@Test public void testAGreaterThanB() {assertEquals(-5, Math.max(-5, -6)); }要注意的是 assertEquals 的參數(shù)順序很重要。它的第一個(gè)應(yīng)該是我們期望的值,通常是一個(gè)我們算好的常數(shù),第二個(gè)參數(shù)就是我們要進(jìn)行的測試。如果你把順序弄反了,JUnit可能會(huì)輸出一些奇怪的錯(cuò)誤報(bào)告。記住, 所有JUnit支持的斷言 都要寫成這個(gè)順序:第一個(gè)是期望值,第二個(gè)是代碼測試結(jié)果。
如果一個(gè)測試斷言失敗了,它會(huì)立即返回,JUnit也會(huì)記錄下這次測試的失敗。一個(gè)測試類可以有很多 @Test 方法,它們可以各自獨(dú)立的進(jìn)行測試,即使有一個(gè)失敗了,其它的測試也會(huì)繼續(xù)進(jìn)行。
寫下你的測試策略
現(xiàn)在假設(shè)我們要測試reverseEnd這個(gè)模塊:
/*** Reverses the end of a string.** For example:* reverseEnd("Hello, world", 5)* returns "Hellodlrow ,"** With start == 0, reverses the entire text.* With start == text.length(), reverses nothing.** @param text non-null String that will have* its end reversed* @param start the index at which the* remainder of the input is* reversed, requires 0 <=* start <= text.length()* @return input text with the substring from* start to the end of the string* reversed*/ static String reverseEnd(String text, int start)我們應(yīng)該在測試時(shí)記錄下我們的測試策略,例如我們是如何分區(qū)的,有哪些特殊值、邊界值等等:
/** Testing strategy** Partition the inputs as follows:* text.length(): 0, 1, > 1* start: 0, 1, 1 < start < text.length(),* text.length() - 1, text.length()* text.length()-start: 0, 1, even > 1, odd > 1** Include even- and odd-length reversals because* only odd has a middle element that doesn't move.** Exhaustive Cartesian coverage of partitions.*/另外,每一個(gè)測試方法都要有一個(gè)小的注解,告訴讀者這個(gè)測試方法是代表我們測試策略中的哪一部分,例如:
// covers test.length() = 0, // start = 0 = text.length(), // text.length()-start = 0 @Test public void testEmpty() {assertEquals("", reverseEnd("", 0)); }閱讀小測試
假設(shè)你在為 max(int a, int b) 寫測試,它是屬于Math.java的. 并且你將JUnit測試放在 MathTest.java文件中.
下面這些文字說明應(yīng)該分別放在哪里?
關(guān)于 a 參數(shù)的分區(qū)策略
[ ] 寫在 Math.java開頭的注釋里
[x] 寫在 MathTest.java開頭的注釋里
[ ] 寫在 max() 開頭的注釋里
[ ] 寫在JUnit測試的注釋里
屬性 @Test
[ ] 在 Math 之前
[ ] 在 MathTest 之前
[ ] 在max() 之前
[x] 在 JUnit 測試之前
注釋 “代表 a < b”
[ ] 寫在 Math.java開頭的注釋里
[ ] 寫在 MathTest.java開頭的注釋里
[ ] 寫在 max() 開頭的注釋里
[x] 寫在JUnit測試的注釋里
注釋 “@返回a和b的最大值”
[ ] 寫在 Math.java開頭的注釋里
[ ] 寫在 MathTest.java開頭的注釋里
[x] 寫在 max() 開頭的注釋里
[ ] 寫在JUnit測試的注釋里
黑盒測試與白盒測試
回想上面提到的:規(guī)格說明是對函數(shù)行為的描述——參數(shù)類型、返回值類型和對它們的約束條件以及參數(shù)和返回值之間的關(guān)系。
黑盒測試意味著只依據(jù)函數(shù)的規(guī)格說明來選擇測試用例,而不關(guān)心函數(shù)是如何實(shí)現(xiàn)的。這也是到目前為止我們的例子里一直在做的。我們在沒有看實(shí)際代碼的情況下分段并且尋找multiply和max的邊界。
白盒測試 的意思是在考慮函數(shù)的實(shí)際實(shí)現(xiàn)方法的前提下選擇測試用例。比如說,如果函數(shù)的實(shí)現(xiàn)中,對不同的輸入采用不同的算法,那么你應(yīng)該根據(jù)這些不同的區(qū)域來分類(譯者注:將輸入分為不同的類,每類輸入將會(huì)觸發(fā)代碼實(shí)現(xiàn)中的一種處理算法)。如果代碼實(shí)現(xiàn)中維護(hù)一個(gè)內(nèi)部緩存來記錄之前得到的輸入的答案,那你應(yīng)該測試重復(fù)的輸入。
在做白盒測試時(shí)。你必須注意:你的測試用例不需要嘗試規(guī)格說明中沒有明確要求的實(shí)現(xiàn)行為。例如,如果規(guī)格說明中說“如果輸入沒有格式化,那么將拋出異常”,那么你不應(yīng)該特地的檢查程序是否拋出NullPointerExpection異常,因?yàn)楫?dāng)前的代碼實(shí)現(xiàn)決定了程序有可能拋出這個(gè)異常。在這種情況下,規(guī)格說明允許任何異常被拋出,所以你的測試用例同樣應(yīng)該“寬容”地保留實(shí)現(xiàn)者的自由。我們將會(huì)在這門課接下來的課時(shí)中討論更多關(guān)于規(guī)格說明的問題。
閱讀小練習(xí)
黑盒測試 vs. 白盒測試
思考下面這個(gè)方法:
/*** Sort a list of integers in nondecreasing order. Modifies the list so that * values.get(i) <= values.get(i+1) for all 0<=i<values.length()-1*/ public static void sort(List<Integer> values) {// choose a good algorithm for the size of the listif (values.length() < 10) {radixSort(values);} else if (values.length() < 1000*1000*1000) {quickSort(values);} else {mergeSort(values);} }下面哪一個(gè)是白盒測試中產(chǎn)生的邊界值?
- [ ] values = [] (the empty list)
- [ ] values = [1, 2, 3]
- [x] values = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
- [ ] values = [0, 0, 1, 0, 0, 0, 0]
覆蓋率
一種判斷測試的好壞的方法就是看該測試對軟件的測試程度。這種測試程度也稱為“覆蓋率”。以下是常見的三種覆蓋率:
- 聲明覆蓋率: 每一個(gè)聲明都被測試到了嗎?
- 分支覆蓋率:對于每一個(gè)if 或 while 等等控制操作,它們的分支都被測試過嗎?
- 路徑覆蓋率: 每一種分支的組合路徑都被測試過嗎?
其中,分支覆蓋率要比聲明覆蓋率嚴(yán)格(需要更多的測試),路徑覆蓋率要比分支覆蓋率嚴(yán)格。在工業(yè)界,100%的聲明覆蓋率一個(gè)普遍的要求,但是這有時(shí)也是不可能實(shí)現(xiàn)的,因?yàn)闀?huì)存在一些“不可能到達(dá)的代碼”(例如有一些斷言)。100%的分支覆蓋率是一種很高的要求,對于軍工/安全關(guān)鍵的軟件可能會(huì)有此要求 (e.g., MC/DC, modified condition/decision coverage)。不幸的是,100%的路徑覆蓋率是不可能的,因?yàn)檫@會(huì)讓測試用例空間以指數(shù)速度增長。
一個(gè)標(biāo)準(zhǔn)的方法就是不斷地增加測試用例直到覆蓋率達(dá)到了預(yù)定的要求。在實(shí)踐中,聲明覆蓋通常用覆蓋率工具進(jìn)行計(jì)數(shù)。利用這樣的工具,白盒測試會(huì)變得很容易,你只需要不斷地調(diào)整覆蓋的地方,直到所有重要的聲明都被覆蓋到。
在Eclipse中有一個(gè)好用的代碼覆蓋率工具 EclEmma 。如上圖所示,EclEmma會(huì)將被執(zhí)行過的代碼用綠色標(biāo)出,沒有被執(zhí)行的代碼用紅色標(biāo)出。對于一個(gè)分支語句,如果它的一個(gè)分支一直沒有被執(zhí)行,那么這個(gè)分支判斷語句會(huì)被標(biāo)為黃色。例如上圖中,我們發(fā)現(xiàn)for循環(huán)中的if語句每一次都是假的,我們下一步要做的就是調(diào)整測試用例使得這個(gè)判斷可以為真。
閱讀小練習(xí)
使用覆蓋率工具
對于現(xiàn)在的Eclipse, EclEmma 已經(jīng)安裝了,我們直接使用即可。
現(xiàn)在我們創(chuàng)建一個(gè)類 Hailstone.java
public class Hailstone {public static void main(String[] args) {int n = 3;while (n != 1) {if (n % 2 == 0) {n = n / 2;} else {n = 3 * n + 1;}}} }利用EclEmma 運(yùn)行main函數(shù), Run → Coverage As → Java Application.并改變n的初始化值,觀察EclEmma 標(biāo)出行顏色的變化。
當(dāng)n = 3時(shí),n = n/2這一行是什么顏色 ?
綠
當(dāng)n = 16時(shí),n = 3 * n + 1這一行是什么顏色 ?
紅
當(dāng)n的初始值是多少時(shí),行while (n != 1)會(huì)變成黃色 ?
1
單元測試 vs. 集成測試和樁(Stubs)
我們已經(jīng)討論過“單元測試”——對孤立的模塊進(jìn)行測試。這使得debugging變得簡單,當(dāng)一個(gè)單元測試報(bào)錯(cuò)是,我們只需要在這個(gè)單元找bug,而不是在整個(gè)程序去找。
與此相對應(yīng)的,“集成測試”是對于組合起來的模塊進(jìn)行測試,甚至是整個(gè)程序。如果集成測試報(bào)錯(cuò),我們就只能在大的范圍去找了。但是這種測試依然是必要的,因?yàn)槌绦蚪?jīng)常由于模塊之間的交互而產(chǎn)生bug。例如,一個(gè)模塊的輸入是另一個(gè)模塊的輸出,但是設(shè)計(jì)者在設(shè)計(jì)模塊的時(shí)候?qū)⑤斎胼敵鲱愋团e(cuò)了。另外,如果我們已經(jīng)做好單元測試了,即我們可以確性各個(gè)單元獨(dú)立的正確性,我們的搜索bug的范圍也會(huì)小很多。
下面假設(shè)你在設(shè)計(jì)一個(gè)搜索引擎。其中有兩個(gè)模塊 getWebPage(), extractWords() ,其中 getWebPage()負(fù)責(zé)下載網(wǎng)頁,extractWords() 負(fù)責(zé)將頁面內(nèi)容拆成一個(gè)個(gè)詞匯:
/** @return the contents of the web page downloaded from url */ public static String getWebPage(URL url) {...}/** @return the words in string s, in the order they appear, * where a word is a contiguous sequence of * non-whitespace and non-punctuation characters */ public static List<String> extractWords(String s) { ... }而這兩個(gè)模塊又是被另一個(gè)模塊 makeIndex()作為網(wǎng)絡(luò)爬蟲的一部分使用的:
/** @return an index mapping a word to the set of URLs * containing that word, for all webpages in the input set */ public static Map<String, Set<URL>> makeIndex(Set<URL> urls) { ...for (URL url : urls) {String page = getWebPage(url);List<String> words = extractWords(page);...}... }我們的測試可以分為:
- 對getWebPage()進(jìn)行單元測試,輸入不同的 URLs
- 對extractWords()進(jìn)行單元測試,輸入不同的字符串
- 對 makeIndex() 進(jìn)行單元測試,輸入不同的 URLs
測試員有時(shí)會(huì)犯這樣一個(gè)錯(cuò)誤:extractWords() 的測試用例依賴于getWebPage() 的正確性。正如前面所提到的,單元測試應(yīng)該盡可能將模塊孤立起來。如果我們在對extractWords() 的測試輸入進(jìn)行分區(qū)后,其值中包含有 getWebPage() 的輸出,那么如果getWebPage() 本身就有bug,程序的測試將變得不可信!正確的做法應(yīng)該是先組建好獨(dú)立的測試用例,例如一些下載好的網(wǎng)頁,將其作為測試用例進(jìn)行測試。
注意到 makeIndex() 的單元測試并不能完全孤立,因?yàn)槲覀冊跍y試它的時(shí)候?qū)嶋H上也測試了它調(diào)用的模塊。如果測試失敗,這些bug也可能來自于它調(diào)用過的模塊之中——這也是為什么我們要先單元測試 getWebPage() 和 extractWords() ,這樣一來我們就能確定bug出現(xiàn)在鏈接這些模塊的代碼之中。
如果我們要做更高于 makeIndex()這一層的測試,我們將它調(diào)用的模塊寫成樁 。例如,一個(gè) getWebPage() 的樁不會(huì)真正去訪問網(wǎng)頁,而是返回一個(gè)預(yù)先設(shè)置好的網(wǎng)頁(mock web page),不管參數(shù)URL是什么。一個(gè)類的樁通常被稱為“模擬對象”( mock object)。在構(gòu)建大型系統(tǒng)的時(shí)候樁是一種重要的手段,但是在本門課程中我們不會(huì)使用它。
譯者注:關(guān)于mocks、stubs、fakes這些概念,可以參考:
- https://en.wikipedia.org/wiki/Mock_object
- 軟件測試中Mock和Stub
自動(dòng)化測試和回歸測試
沒有什么能比自動(dòng)化更能讓測試簡單的東西了。自動(dòng)化測試(Automated testing)是指自動(dòng)地運(yùn)行測試對象,輸入對應(yīng)的測試用例,并記錄結(jié)果的測試。
能夠進(jìn)行自動(dòng)化測試的代碼稱作測試驅(qū)動(dòng)(test driver,也被稱作test harness 或者 test runner)。一個(gè)測試驅(qū)動(dòng)不應(yīng)該在測試的時(shí)候停下來等待你的輸入,而是自動(dòng)調(diào)用模塊輸入測試用例進(jìn)行測試,最后的結(jié)果應(yīng)該是“測試完成,一切正?!被蛘摺斑@些測試發(fā)了報(bào)錯(cuò):.....”。一個(gè)好的測試架構(gòu),例如JUnit,允許你構(gòu)建這樣的測試驅(qū)動(dòng)。
注意到自動(dòng)化測試架構(gòu)比如JUnit讓測試變得簡單,但是你還是要自己去構(gòu)建好的測試用例。“自動(dòng)化生成測試用例”是一個(gè)很難的問題,目前還處于活躍的研究之中。
要特別注意的是,當(dāng)你修改你的代碼后,別忘了重新運(yùn)行之前的自動(dòng)化測試。軟件工程師經(jīng)常遭遇修改大型/復(fù)雜程序所帶來的痛苦,不論是修改一個(gè)bug、增加一個(gè)新的功能、優(yōu)化一段代碼的性能,都有可能帶來新的問題。無論什么時(shí)候,自動(dòng)化測試都能保證軟件最重要的底線——行為和結(jié)果是正確的,即使只是一小段測試。我們稱修改代碼帶來新的bug的現(xiàn)象為“回歸”,而在修改后重新運(yùn)行所有的測試稱為“回歸測試”。
一個(gè)好的測試應(yīng)該是能發(fā)現(xiàn)bug的,你應(yīng)該不斷的充實(shí)你的測試用例。所以無論什么時(shí)候修改了一個(gè)bug,記得將導(dǎo)致bug的輸入添加到你的測試用例里,并在以后的回歸測試中去使用它——畢竟這個(gè)bug已經(jīng)出現(xiàn)了,說明它可能是一個(gè)很容易犯的錯(cuò)誤。
這些思想也是“測試優(yōu)先debugging”的核心,當(dāng)bug出現(xiàn)時(shí),立刻將觸發(fā)bug的輸入存放到測試用例中,當(dāng)你修復(fù)bug后,再次運(yùn)行這些 (譯者注:注意不僅是觸發(fā)bug的輸入)測試,如果它們都通過的話,你的debug也就完成了。
在實(shí)踐中,自動(dòng)化測試和回歸測試通常結(jié)合起來使用。因?yàn)榛貧w測試只有自動(dòng)化才可行(不然大量的測試沒法實(shí)現(xiàn))。反過來,如果你已經(jīng)構(gòu)建了自動(dòng)化測試,你通常也會(huì)用它來防止回歸的發(fā)生。所以自動(dòng)化回歸測試(automated regression testing)是軟件工程里的一個(gè)“最佳實(shí)踐”(best-practice)。
閱讀小練習(xí)
回歸測試
以下哪一個(gè)選項(xiàng)是對回歸測試的最好定義 ?
[x] 當(dāng)你改變代碼后應(yīng)該再次進(jìn)行測試
[ ] 代碼的每一個(gè)模塊都應(yīng)該有能夠完全測試它的測試
[ ] 測試應(yīng)該在寫代碼之前完成,以此來檢查你寫的規(guī)格說明
[ ] 當(dāng)新的測試報(bào)錯(cuò)時(shí),你應(yīng)該重新運(yùn)行之前的所有版本的代碼直到找到開始引入這個(gè)bug的版本。
自動(dòng)化測試
什么情況下應(yīng)該重新運(yùn)行所有的 JUnit 測試?
[x] 在使用 git add/commit/push之前
[x] 在優(yōu)化一個(gè)函數(shù)的性能后
[ ] 在使用覆蓋率工具時(shí)
[x] 在修改一個(gè)bug后
測試方法
以下哪一些方法/思想對于“測試優(yōu)先編程”中未寫代碼之前選擇測試用例是有幫助的?
- [x] 黑盒
- [ ] 回歸
- [ ] 靜態(tài)類型
- [x] 分區(qū)
- [x] 分區(qū)邊界
- [ ] 白盒
- [ ] 覆蓋率
總結(jié)
在這個(gè)reading中,我們學(xué)到了以下知識(shí):
- 測試優(yōu)先編程——在寫代碼前先寫好測試用例,盡早發(fā)現(xiàn)bug。
- 利用分區(qū)與分區(qū)邊界來選擇測試用例。
- 白盒測試與聲明覆蓋率。
- 單元測試——將測試模塊隔離開來。
- 自動(dòng)化回歸測試杜絕新的bug產(chǎn)生。
還記得好軟件具備的三個(gè)屬性嗎?試著將它們和這篇reading的內(nèi)容聯(lián)系起來:
- 遠(yuǎn)離bug 測試的意義在于發(fā)現(xiàn)程序中的bug,而“測試優(yōu)先編程”的價(jià)值在于盡可能早的發(fā)現(xiàn)這些bug。
- 易讀性 額.......測試并不會(huì)使代碼審查變得容易,但是我們也要注意正確書寫測試注釋。
- 可改動(dòng)性 我們針對改動(dòng)后的程序進(jìn)行測試時(shí)只需要依賴規(guī)格說明中的行為描述。(譯者注:再說一遍,這里的測試針對的是正確性而不是魯棒性)。另外,當(dāng)我們完成修改后,自動(dòng)化回歸測試能夠幫助我們杜絕新的bug產(chǎn)生。
轉(zhuǎn)載于:https://www.cnblogs.com/liqiuhao/p/8512205.html
總結(jié)
以上是生活随笔為你收集整理的麻省理工18年春软件构造课程阅读03“测试”的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: etlgr是什么服务器_ETL是指什么
- 下一篇: 关于游戏平衡性——王者荣耀英雄伤害数值参