麻省理工18年春軟件構造課程閱讀03「測試」

<font size="3"> **本文內容來自[MIT_6.031_sp18: Software Construction](http://web.mit.edu/6.031/www/sp18/)課程的Readings部分,採用[CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/)協議。**html

因爲咱們學校(哈工大)大二軟件構造課程的大部分素材取自此,也是推薦的閱讀材料之一,因而打算作一些翻譯工做,本身學習的同時也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有許多練習題,可是沒有標準答案,所給出的答案均爲譯者所寫,有錯誤的地方還請指出。java

<br />git


<br />程序員

譯者:李秋豪 江家偉web

審校:李秋豪算法

V1.0 Tue Mar 6 01:54:34 CST 2018編程

<br />小程序

本次課程的目標

  • 理解測試的意義,並瞭解「測試優先編程」的過程
  • 可以使用「分區」的方法選擇合適的輸入輸出測試用例
  • 可以經過代碼覆蓋率來評價一個測試的好壞
  • 理解黑盒/白盒測試、單元/集成測試、自動化迴歸測試。

<br />api

驗證(Validation)

「測試」是「驗證」的一種例子。而驗證的目的就是發現程序中的問題,以此提高你對程序正確性的信心。驗證包括:數組

  • 形式推理,即經過理論推理證實程序的正確性。形式推理目前還缺少自動化的工具,一般須要漫長的手工計算。即便是這樣,一些關鍵性的小程序也是須要被證實的,例如操做系統的調度程序、虛擬機裏的字節碼解釋器,或者是 文件系統.
  • 代碼審查. 即讓別人仔細的閱讀、審校、評價你的代碼,這也是發現bug的一個經常使用方法,咱們會在下一個reading裏面介紹這種方法。
  • 測試.即選擇合適的輸入輸出用例,經過運行程序檢查程序的問題。

即便是最優的驗證,程序也不可能達到十全十美,這裏列出了通常狀況下程序的剩餘缺陷率residual defect rates(軟件發行時存在的bug比率) ,這裏的單位是每 kloc (每一千行代碼):

  • 1 - 10 defects/kloc: 常見的工業級軟件。
  • 0.1 - 1 defects/kloc: 高質量驗證後的軟件。例如Java的官方庫可能就是這個級別。
  • 0.01 - 0.1 defects/kloc: 最高級別、軍工/安全關鍵軟件。例如NASA或者像Praxis這樣的公司(譯者注:1.歐洲著名的電力行業信息化解決方案專家,隸屬於世界第三大的電力集團E.ON,總部位於德國。2.美國教師資格證考試 這裏不知道說的是第一個仍是第二個(體現一下幽默感?))

這看起來讓人沮喪,想想,若是你寫了100萬行的大型程序,那你極可能沒檢查出1000個bug!

<br />

爲何軟件測試很困難

這裏有一些在工業界測試產品經常使用的方法,但是它們在軟件行業沒法發揮應有的做用。

**盡力測試(嘗試全部的可能):**這一般是不可行的,由於大多數狀況下輸入空間會很是大,例如僅僅是一個浮點數乘法a*b ,其總共的取值就有2^64種可能性!

隨機測試 (試一下看看行不行): 這一般難以發現bug,除非這個程序處處都是bug以致於隨便一個輸入都能崩潰。即便咱們修復了測試出來的bug,隨機的輸入也不能使咱們對程序的正確性很肯定。

基於統計方法的測試:遺憾的是,這種方法對軟件不那麼奏效。在物理系統裏,工程師能夠經過特定的方法加速實驗的進程,例如在一天的時間裏打開關閉一個冰箱門一千次,以此來模擬幾年的正常使用,最終獲得產品的」失敗率「。之後的測試結果也將會集中分佈在這個比率左右,工程師們就對這個比率進行進一步的研究。可是軟件的行爲一般是離散且不可預測的。程序可能在上一秒還徹底正常的工做,忽然就崩潰了,也可能對於大多數輸入都沒問題,對於一個值就崩潰了(沒有預兆,更談不上失敗率,因此很難提早作好監測的準備),例如 有名的奔騰處理器除法bug ,在90億次的除法中才可能會有一個錯誤。

綜上,咱們必須系統並且當心的選擇測試用例,這也是下面要講的。

<br />

閱讀小練習

測試基礎

阿麗亞娜5型火箭,爲歐洲空間局研發的民用衛星一次性運載火箭,名稱來源於神話人物阿麗雅杜妮(Ariadne)的法語拼寫。1996年6月4日,在風和日麗的法屬圭亞那太空發射場,阿麗亞娜5型運載火箭首航,計劃運送4顆太陽風觀察衛星到預約軌道。但在點火升空以後的40秒後,在4000米高空,這個價值5億美圓的運載系統就發生了爆炸,瞬間灰飛煙滅化爲烏有。

爆炸緣由因爲火箭某段控制程序直接移植自阿麗亞娜4型火箭,其中一個須要接收64位數據的變量爲了節省存儲空間而使用了16位字節,從而在控制過程當中產生了整數溢出,致使導航系統對火箭控制失效,程序進入異常處理模塊,引爆自毀。

這個故事告訴了咱們什麼?

  • [x] 即便是高度關鍵性的程序也可能有bug
  • [ ] 測試全部可能輸入是解決這樣的問題的最好辦法
  • [x] 與不少物理工程學上的系統不一樣,軟件的行爲是離散的
  • [ ] 靜態檢查有助於發現這個bug

靜態類型檢查不會檢測到此錯誤,由於代碼有意(強轉)將64位精度轉換爲16位精度。

<br />

測試應該具有的心態(Putting on Your Testing Hat)

測試須要一個正確的態度:當你在寫一個程序的時候,你的心態必定是讓這個程序正常運行,可是做爲一個測試者,你應該千方百計讓程序崩潰

這是一個隱晦但重要的區別,一個優秀的測試員會「揮舞的重錘敲打代碼可能有問題的地方」,而不是「當心的呵護它」。

<br />

測試優先編程(Test-first Programming)

測試開始的時間應該儘可能早,而且要頻繁地測試。當你有一大堆未經驗證的代碼時,不要把測試工做留到最後。把測試工做留到最後只會讓調試的時間更久而且調試過程更加痛苦,由於你的代碼將會充斥着bug。反之,若是你在編碼的過程當中就進行測試,狀況就會好的多。

在測試優先編程中,測試程序先於代碼完成。編寫一個函數應該按以下步驟進行:

  1. 爲函數寫一個規格說明。
  2. 爲上一步的規格說明寫一些測試用例。
  3. 編寫實際代碼。一旦你的代碼經過了全部你寫的測試用例,這個函數就算完成了。

規格說明描述了這個函數的輸入輸出行爲。它肯定了函數參數的類型和對它們的全部約束(例如sqrt函數的參數必須是非負的)。它還定義了函數的返回值類型以及返回值和輸入之間的關係。你已經在這門課中對許多問題都查看並使用過規格說明。在代碼中,規格說明包括了函數簽名和一些描述函數功能的註釋。咱們將會在接下來的幾節課裏討論更多關於規格說明的問題。

先完成測試用例的編寫可以讓你更好地理解規格說明。規格說明也可能存在問題——不正確、不完整、模棱兩可、缺失邊界狀況。先嚐試編寫測試用例,能夠在你浪費時間實現一個有問題的規格說明以前發現這些問題。

<br />

經過分區的方法選擇測試用例

選擇合適的測試用例是一個具備挑戰性可是有缺的問題。咱們即但願測試空間足夠小,以便可以快速完成測試,又但願測試用例可以驗證儘量多的狀況。

爲了達到這個目的,咱們能夠先將輸入空間劃分爲幾個子域(subdomains) ,每個子域都是一類類似的數據。如上圖所示,咱們在每一個子域中選取一些數據,它們合併起來就是咱們須要的輸入用例。

分區背後的原理在於同一類型的數據在程序中的行爲大多相似,因此咱們能夠用一小部分表明總體的行爲。這個方法的優勢在於強迫程序相應輸入空間裏的不一樣地方,有效的利用了測試資源。

若是咱們要確保測試的輸出可以覆蓋輸出空間的不一樣地方,也能夠將輸出空間劃分爲幾個子域(哪些輸出表明程序發生了類似的行爲)。大多數狀況下,對輸入分區就足夠了

例子1: BigInteger.multiply()

如今讓咱們來看一個例子。 BigInteger 是Java庫中的一個類,它可以表示任意大小的整數。同時,它有一個multiply 方法,可以對兩個BigInteger類型的值進行相乘操做:

/**
 * @param val another BigInteger
 * @return a BigInteger whose value is (this * val).
 */
public BigInteger multiply(BigInteger val)

例如,計算ab的值:

BigInteger a = ...;
BigInteger b = ...;
BigInteger ab = a.multiply(b);

這個例子顯示即便只有一個參數,這個操做實際上有兩個操做符:你調用這個方法所在的對象(上面是a ),以及你傳入的參數(上面是b )。(在Python中,接受方法調用的對象會顯式以self被聲明。在Java中你不須要聲明這個對象,它隱式的被稱做this )咱們能夠把 multiply 當作一個有兩個參數的方法,參數的類型是 BigInteger ,而且輸出的類型也是 BigInteger 即:

multiply : BigInteger × BigInteger → BigInteger

因此咱們的輸入空間是二維的,用二維點陣(a,b)表示。如今咱們對其進行分區,想想乘法是怎麼工做的,咱們能夠將點陣初步分爲如下四個區:

  • a和b都是正整數
  • a和b都是負整數
  • a是正整數,b是負整數
  • b是正整數,a是負整數

這裏也有一些特殊的狀況要單獨分出來:0 1 -1

  • a或b是1\0\-1

最後,做爲一個認真的測試員,咱們還要想想BigInteger的乘法多是怎麼運算的:它可能在輸入數據絕對值較小時使用 intlong ,這樣運算起來快一些,只有當數據很大時纔會使用更費勁的存儲方法(例如列表)。因此咱們也應該將對數據的大小進行分區:

  • a或b較小
  • a或b的絕對值大於Long.MAX_VALUE ,即Java原始整型的最大值,大約是2^63。

如今咱們能夠將上面劃分的區域整合起來,獲得最終劃分的點陣:

  • 0
  • 1
  • -1
  • 較小正整數
  • 較小負整數
  • 大正整數
  • 大負整數

因此咱們一共能夠獲得 7 × 7 = 49 個分區,它們徹底覆蓋了a和b組成的全部輸入空間。而後從這個」柵欄「裏的每一個區選取各自的測試用例,例如:

  • (a,b) = (-3, 25) 表明 (小負整數, 小正整數)
  • (a,b) = (0, 30) 表明 (0, 小正整數)
  • (a,b) = (2^100, 1) 表明 (大正整數, 1)
  • 等等

例子2: max()

如今咱們看看Java庫中的另外一個例子:針對整數int max() 函數,它屬於 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 (譯者注:這裏的乘號不表明乘法,而是一種封閉的二元運算關係,參見近世代數)

經過描述分析,咱們能夠將其分區爲:

  • a < b
  • a = b
  • a > b

因此能夠選擇如下測試用例:

  • (a, b) = (1, 2) 表明 a < b
  • (a, b) = (9, 9) 表明 a = b
  • (a, b) = (-5, -6) 表明 a > b

注意分區之間的「邊界」

bug常常會在各個分區的邊界處發生,例如:

  • 在正整數和負整數之間的0
  • 數字類型的最大值和最小值,例如 intdouble
  • 空集,例如空的字符串,空的列表,空的數組
  • 集合類型中的第一個元素或最後一個元素

爲何這些邊界的地方常常產生bug呢?一個很重要的緣由就是程序員常常犯**「丟失一個(off-by-one mistakes)」**的錯誤。例如將<=寫成< ,或者將計數器用0來初始化而不是1。另一個緣由就是邊界處的值可能須要用特殊的行爲來處理,例如當int類型的變量達到最大值之後,再對其加正整數反而會變成負數。

因此,咱們在分區後,測試用例不要忘了加上邊界上的值,如今從新作一下上面那個例子:

max : int × int → int.

分區:

  • a與b的關係
    • a < b
    • a = b
    • a > b
  • a的值
    • a = 0
    • a < 0
    • a > 0
    • a = 最小的整數
    • a = 最大的整數
  • value of b
    • b = 0
    • b < 0
    • b > 0
    • b = 最小的整數
    • b = 最大的整數

如今咱們再次選取測試用例覆蓋上面的分區和邊界值:

  • (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

覆蓋分區的兩個極限狀況

在分區後,咱們能夠選擇「盡力(how exhaustive we want)」的程度來測試咱們的分區,這裏有兩個極限狀況:

  • 徹底笛卡爾乘積 即對每個存在組合都進行測試。例如在第一個例子multiply中,咱們一共使用了 7 × 7 = 49 個測試用例,每個組合都用上了。對於第二個例子,就會是 3 × 5 × 5 = 75個測試用例。要注意的是,實際上有一些組合是不存在的,例如 a < b, a=0, b=0。
  • 每個分區被覆蓋便可 即每個分區至少被覆蓋一次。例如咱們在第二個例子max中只使用了5個測試用例,可是這5個用例覆蓋到了咱們的三維輸入空間的全部分區。

在實際測試中咱們一般在這兩個極限中折中,這種折中是基於人們的經驗,對代碼的獲取度(黑白盒測試)、以及對代碼的覆蓋率,這些咱們會在後面講到。

閱讀小練習

分區

思考下面這個規格說明:

/**
 * 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 參數進行測試,下面的哪個分區是合理的 ?

  • [ ] 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()

譯者注:要特別注意的是,本文談到的都是對程序正確性進行測試,即輸入都是規格說明裏面的合法值。至於那些非法的值則是對魯棒性(robust)或者安全性的測試。

對於 text 參數進行測試,下面的哪個分區是合理的 ?

  • [ ] text 包含一些數字; text不包含字母, 可是包含一些數字; text 既不包含字母,也不包含數字
  • [ ] text.length() = 0; text.length() > 0
  • [x] text.length() = 0; text.length()-start 是奇數; text.length()-start 是偶數(譯者注,這個選項是第二個的超集,多的地方在於奇數偶數的判斷,緣由在於若是一個字符串字符的個數是奇數個,那麼中間的那個字符就不須要移動位置了,這可能須要特殊的行爲來處理,也多是bug產生的緣由)
  • [ ] 測試0到100個字符的全部字符串

<br />

用JUnit作自動化單元測試

一個良好的測試程序應該測試軟件的每個模塊(方法或者類)。若是這種測試每次是對一個孤立的模塊單獨進行的,那麼這就稱爲「單元測試」。單元測試的好處在於debug,若是你發現一個單元測試失敗了,那麼bug極可能就在這個單元內部,而不是軟件的其餘地方。

JUnit 是Java中一個被普遍只用的測試庫,咱們在之後的課程中也會大量使用它。一個JUnit測試單元是以一個方法(method)寫出的,其首部有一個 @Test聲明。一個測試單元一般含有對測試的模塊進行的一次或屢次調用,同時會用斷言檢查模塊的返回值,好比 assertEquals, assertTrue, 和 assertFalse.i

例如,咱們對上面提到的 Math.max() 模塊進行測試,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 的參數順序很重要。它的第一個應該是咱們指望的值,一般是一個咱們算好的常數,第二個參數就是咱們要進行的測試。若是你把順序弄反了,JUnit可能會輸出一些奇怪的錯誤報告。記住, 全部JUnit支持的斷言 都要寫成這個順序:第一個是指望值,第二個是代碼測試結果。

若是一個測試斷言失敗了,它會當即返回,JUnit也會記錄下此次測試的失敗。一個測試類能夠有不少 @Test 方法,它們能夠各自獨立的進行測試,即便有一個失敗了,其它的測試也會繼續進行。

<br />

寫下你的測試策略

如今假設咱們要測試reverseEnd這個模塊:

/**
 * 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)

咱們應該在測試時記錄下咱們的測試策略,例如咱們是如何分區的,有哪些特殊值、邊界值等等:

/*
 * 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.
 */

另外,每個測試方法都要有一個小的註解,告訴讀者這個測試方法是表明咱們測試策略中的哪一部分,例如:

// covers test.length() = 0,
//        start = 0 = text.length(),
//        text.length()-start = 0
@Test public void testEmpty() {
    assertEquals("", reverseEnd("", 0));
}

閱讀小測試

假設你在爲 max(int a, int b) 寫測試,它是屬於Math.java的. 而且你將JUnit測試放在 MathTest.java文件中.

下面這些文字說明應該分別放在哪裏?

關於 a 參數的分區策略

  • [ ] 寫在 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測試的註釋裏

<br />

黑盒測試與白盒測試

回想上面提到的:規格說明是對函數行爲的描述——參數類型、返回值類型和對它們的約束條件以及參數和返回值之間的關係。

黑盒測試意味着只依據函數的規格說明來選擇測試用例,而不關心函數是如何實現的。這也是到目前爲止咱們的例子裏一直在作的。咱們在沒有看實際代碼的狀況下分段而且尋找multiplymax的邊界。

白盒測試 的意思是在考慮函數的實際實現方法的前提下選擇測試用例。好比說,若是函數的實現中,對不一樣的輸入採用不一樣的算法,那麼你應該根據這些不一樣的區域來分類(譯者注:將輸入分爲不一樣的類,每類輸入將會觸發代碼實現中的一種處理算法)。若是代碼實現中維護一個內部緩存來記錄以前獲得的輸入的答案,那你應該測試重複的輸入。

在作白盒測試時。你必須注意:你的測試用例不須要嘗試規格說明中沒有明確要求的實現行爲。例如,若是規格說明中說「若是輸入沒有格式化,那麼將拋出異常」,那麼你不該該特意的檢查程序是否拋出NullPointerExpection異常,由於當前的代碼實現決定了程序有可能拋出這個異常。在這種狀況下,規格說明容許任何異常被拋出,因此你的測試用例一樣應該「寬容」地保留實現者的自由。咱們將會在這門課接下來的課時中討論更多關於規格說明的問題。

閱讀小練習

黑盒測試 vs. 白盒測試

思考下面這個方法:

/**
 * 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 list
    if (values.length() < 10) {
        radixSort(values);
    } else if (values.length() < 1000*1000*1000) {
        quickSort(values);
    } else {
        mergeSort(values);
    }
}

下面哪個是白盒測試中產生的邊界值?

  • [ ] 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]

<br />

覆蓋率

一種判斷測試的好壞的方法就是看該測試對軟件的測試程度。這種測試程度也稱爲「覆蓋率」。如下是常見的三種覆蓋率:

  • 聲明覆蓋率: 每個聲明都被測試到了嗎?
  • 分支覆蓋率:對於每個ifwhile 等等控制操做,它們的分支都被測試過嗎?
  • 路徑覆蓋率: 每一種分支的組合路徑都被測試過嗎?

其中,分支覆蓋率要比聲明覆蓋率嚴格(須要更多的測試),路徑覆蓋率要比分支覆蓋率嚴格。在工業界,100%的聲明覆蓋率一個廣泛的要求,可是這有時也是不可能實現的,由於會存在一些「不可能到達的代碼」(例若有一些斷言)。100%的分支覆蓋率是一種很高的要求,對於軍工/安全關鍵的軟件可能會有此要求 (e.g., MC/DC, modified condition/decision coverage)。不幸的是,100%的路徑覆蓋率是不可能的,由於這會讓測試用例空間以指數速度增加。

一個標準的方法就是不斷地增長測試用例直到覆蓋率達到了預約的要求。在實踐中,聲明覆蓋一般用覆蓋率工具進行計數。利用這樣的工具,白盒測試會變得很容易,你只須要不斷地調整覆蓋的地方,直到全部重要的聲明都被覆蓋到。

在Eclipse中有一個好用的代碼覆蓋率工具 EclEmma 。如上圖所示,EclEmma會將被執行過的代碼用綠色標出,沒有被執行的代碼用紅色標出。對於一個分支語句,若是它的一個分支一直沒有被執行,那麼這個分支判斷語句會被標爲黃色。例如上圖中,咱們發現for循環中的if語句每一次都是假的,咱們下一步要作的就是調整測試用例使得這個判斷能夠爲真。

閱讀小練習

使用覆蓋率工具

對於如今的Eclipse, EclEmma 已經安裝了,咱們直接使用便可。

如今咱們建立一個類 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 運行main函數, Run → Coverage As → Java Application.並改變n的初始化值,觀察EclEmma 標出行顏色的變化。

n = 3時,n = n/2這一行是什麼顏色 ?

n = 16時,n = 3 * n + 1這一行是什麼顏色 ?

n的初始值是多少時,行while (n != 1)會變成黃色 ?

1

<br />

單元測試 vs. 集成測試和樁(Stubs)

咱們已經討論過「單元測試」——對孤立的模塊進行測試。這使得debugging變得簡單,當一個單元測試報錯是,咱們只須要在這個單元找bug,而不是在整個程序去找。

與此相對應的,「集成測試」是對於組合起來的模塊進行測試,甚至是整個程序。若是集成測試報錯,咱們就只能在大的範圍去找了。可是這種測試依然是必要的,由於程序常常因爲模塊之間的交互而產生bug。例如,一個模塊的輸入是另外一個模塊的輸出,可是設計者在設計模塊的時候將輸入輸出類型弄錯了。另外,若是咱們已經作好單元測試了,即咱們能夠確性各個單元獨立的正確性,咱們的搜索bug的範圍也會小不少。

下面假設你在設計一個搜索引擎。其中有兩個模塊 getWebPage(), extractWords() ,其中 getWebPage()負責下載網頁,extractWords() 負責將頁面內容拆成一個個詞彙:

/** @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) { ... }

而這兩個模塊又是被另外一個模塊 makeIndex()做爲網絡爬蟲的一部分使用的:

/** @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()進行單元測試,輸入不一樣的 URLs
  • extractWords()進行單元測試,輸入不一樣的字符串
  • makeIndex() 進行單元測試,輸入不一樣的 URLs

測試員有時會犯這樣一個錯誤:extractWords() 的測試用例依賴於getWebPage() 的正確性。正如前面所提到的,單元測試應該儘量將模塊孤立起來。若是咱們在對extractWords() 的測試輸入進行分區後,其值中包含有 getWebPage() 的輸出,那麼若是getWebPage() 自己就有bug,程序的測試將變得不可信!正確的作法應該是先組建好獨立的測試用例,例如一些下載好的網頁,將其做爲測試用例進行測試。

注意到 makeIndex() 的單元測試並不能徹底孤立,由於咱們在測試它的時候實際上也測試了它調用的模塊。若是測試失敗,這些bug也可能來自於它調用過的模塊之中——這也是爲何咱們要先單元測試 getWebPage()extractWords() ,這樣一來咱們就能肯定bug出如今連接這些模塊的代碼之中。

若是咱們要作更高於 makeIndex()這一層的測試,咱們將它調用的模塊寫成 。例如,一個 getWebPage() 的樁不會真正去訪問網頁,而是返回一個預先設置好的網頁(mock web page),無論參數URL是什麼。一個類的樁一般被稱爲「模擬對象」( mock object)。在構建大型系統的時候樁是一種重要的手段,可是在本門課程中咱們不會使用它。

譯者注:關於mocks、stubs、fakes這些概念,能夠參考:

<br />

自動化測試和迴歸測試

沒有什麼能比自動化更能讓測試簡單的東西了。**自動化測試(Automated testing)**是指自動地運行測試對象,輸入對應的測試用例,並記錄結果的測試。

可以進行自動化測試的代碼稱做測試驅動(test driver,也被稱做test harness 或者 test runner)。一個測試驅動不該該在測試的時候停下來等待你的輸入,而是自動調用模塊輸入測試用例進行測試,最後的結果應該是「測試完成,一切正常」或者「這些測試發了報錯:.....」。一個好的測試架構,例如JUnit,容許你構建這樣的測試驅動。

注意到自動化測試架構好比JUnit讓測試變得簡單,可是你仍是要本身去構建好的測試用例。「自動化生成測試用例」是一個很難的問題,目前還處於活躍的研究之中。

要特別注意的是,當你修改你的代碼後,別忘了從新運行以前的自動化測試。軟件工程師常常遭遇修改大型/複雜程序所帶來的痛苦,不管是修改一個bug、增長一個新的功能、優化一段代碼的性能,都有可能帶來新的問題。不管何時,自動化測試都能保證軟件最重要的底線——行爲和結果是正確的,即便只是一小段測試。咱們稱修改代碼帶來新的bug的現象爲「迴歸」,而在修改後從新運行全部的測試稱爲「迴歸測試」。

一個好的測試應該是能發現bug的,你應該不斷的充實你的測試用例。因此不管何時修改了一個bug,記得將致使bug的輸入添加到你的測試用例裏,並在之後的迴歸測試中去使用它——畢竟這個bug已經出現了,說明它多是一個很容易犯的錯誤。

這些思想也是「測試優先debugging」的核心,當bug出現時,馬上將觸發bug的輸入存放到測試用例中,當你修復bug後,再次運行這些 (譯者注:注意不只是觸發bug的輸入)測試,若是它們都經過的話,你的debug也就完成了。

在實踐中,自動化測試和迴歸測試一般結合起來使用。由於迴歸測試只有自動化纔可行(否則大量的測試無法實現)。反過來,若是你已經構建了自動化測試,你一般也會用它來防止迴歸的發生。因此**自動化迴歸測試(automated regression testing)**是軟件工程裏的一個「最佳實踐」(best-practice)。

閱讀小練習

迴歸測試

如下哪個選項是對迴歸測試的最好定義 ?

  • [x] 當你改變代碼後應該再次進行測試

  • [ ] 代碼的每個模塊都應該有可以徹底測試它的測試

  • [ ] 測試應該在寫代碼以前完成,以此來檢查你寫的規格說明

  • [ ] 當新的測試報錯時,你應該從新運行以前的全部版本的代碼直到找到開始引入這個bug的版本。

自動化測試

什麼狀況下應該從新運行全部的 JUnit 測試?

  • [x] 在使用 git add/commit/push以前

  • [x] 在優化一個函數的性能後

  • [ ] 在使用覆蓋率工具時

  • [x] 在修改一個bug後

測試方法

如下哪一些方法/思想對於「測試優先編程」中未寫代碼以前選擇測試用例是有幫助的?

  • [x] 黑盒
  • [ ] 迴歸
  • [ ] 靜態類型
  • [x] 分區
  • [x] 分區邊界
  • [ ] 白盒
  • [ ] 覆蓋率

<br />

總結

在這個reading中,咱們學到了如下知識:

  • 測試優先編程——在寫代碼前先寫好測試用例,儘早發現bug。
  • 利用分區與分區邊界來選擇測試用例。
  • 白盒測試與聲明覆蓋率。
  • 單元測試——將測試模塊隔離開來。
  • 自動化迴歸測試杜絕新的bug產生。

還記得好軟件具有的三個屬性嗎?試着將它們和這篇reading的內容聯繫起來:

  • 遠離bug 測試的意義在於發現程序中的bug,而「測試優先編程」的價值在於儘量早的發現這些bug。
  • 易讀性 額.......測試並不會使代碼審查變得容易,可是咱們也要注意正確書寫測試註釋。
  • 可改動性 咱們針對改動後的程序進行測試時只須要依賴規格說明中的行爲描述。(譯者注:再說一遍,這裏的測試針對的是正確性而不是魯棒性)。另外,當咱們完成修改後,自動化迴歸測試可以幫助咱們杜絕新的bug產生。

</font>

相關文章
相關標籤/搜索