JUnit4是JUnit框架有史以來的最大改進,其主要目標即是利用Java5的Annotation特性簡化測試用例的編寫。java
先 簡單解釋一下什麼是Annotation,這個單詞通常是翻譯成元數據。元數據是什麼?元數據就是描述數據的數據。也就是說,這個東西在Java裏面能夠 用來和public、static等關鍵字同樣來修飾類名、方法名、變量名。修飾的做用描述這個數據是作什麼用的,差很少和public描述這個數據是公 有的同樣。想具體瞭解能夠看Core Java2。廢話很少說了,直接進入正題。程序員
咱們先看一下在JUnit 3中咱們是怎樣寫一個單元測試的。好比下面一個類:
public class AddOperation {
public int add(int x,int y){
return x+y;
}
}web
咱們要測試add這個方法,咱們寫單元測試得這麼寫:
import junit.framework.TestCase;
import static org.junit.Assert.*;
public class AddOperationTest extends TestCase{數據庫
public void setUp() throws Exception {
}編程
public void tearDown() throws Exception {
}數組
public void testAdd() {
System.out.println(\」add\」);
int x = 0;
int y = 0;
AddOperation instance = new AddOperation();
int expResult = 0;
int result = instance.add(x, y);
assertEquals(expResult, result);
}
}框架
能夠看到上面的類使用了JDK5中的靜態導入,這個相對來講就很簡單,只要在import關鍵字後面加上static關鍵字,就能夠把後面的類的static的變量和方法導入到這個類中,調用的時候和調用本身的方法沒有任何區別。ide
咱們能夠看到上面那個單元測試有一些比較霸道的地方,表如今:
1.單元測試類必須繼承自TestCase。
2.要測試的方法必須以test開頭。函數
若是上面那個單元測試在JUnit 4中寫就不會這麼複雜。代碼以下:
import junit.framework.TestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;單元測試
/**
*
* @author bean
*/
public class AddOperationTest extends TestCase{
public AddOperationTest() {
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void add() {
System.out.println(\」add\」);
int x = 0;
int y = 0;
AddOperation instance = new AddOperation();
int expResult = 0;
int result = instance.add(x, y);
assertEquals(expResult, result);
}
}
咱們能夠看到,採用Annotation的JUnit已經不會霸道的要求你必須繼承自TestCase了,並且測試方法也沒必要以test開頭了,只要以@Test元數據來描述便可。
從上面的例子能夠看到在JUnit 4中還引入了一些其餘的元數據,下面一一介紹:
@Before:
使用了該元數據的方法在每一個測試方法執行以前都要執行一次。
@After:
使用了該元數據的方法在每一個測試方法執行以後要執行一次。
注意:@Before和@After標示的方法只能各有一個。這個至關於取代了JUnit之前版本中的setUp和tearDown方法,固然你還能夠繼續叫這個名字,不過JUnit不會霸道的要求你這麼作了。
@Test(expected=*.class)
在JUnit4.0以前,對錯誤的測試,咱們只能經過fail來產生一個錯誤,並在try塊裏面assertTrue(true)來測試。如今,經過@Test元數據中的expected屬性。expected屬性的值是一個異常的類型
@Test(timeout=xxx):
該元數據傳入了一個時間(毫秒)給測試方法,
若是測試方法在制定的時間以內沒有運行完,則測試也失敗。
@ignore:
該 元數據標記的測試方法在測試中會被忽略。當測試的方法尚未實現,或者測試的方法已通過時,或者在某種條件下才能測試該方法(好比須要一個數據庫聯接,而 在本地測試的時候,數據庫並無鏈接),那麼使用該標籤來標示這個方法。同時,你能夠爲該標籤傳遞一個String的參數,來代表爲何會忽略這個測試方 法。好比:@lgnore(「該方法尚未實現」),在執行的時候,僅會報告該方法沒有實現,而不會運行測試方法。
咱們在編寫大型程序的時候,須要寫成千上萬個 方法或函數,這些函數的功能可能很強大,但咱們在程序中只用到該函數的一小部分功能,而且通過調試能夠肯定,這一小部分功能是正確的。可是,咱們同時應該 確保每個函數都徹底正確,由於若是咱們從此若是對程序進行擴展,用到了某個函數的其餘功能,而這個功能有bug的話,那絕對是一件很是鬱悶的事情。因此 說,每編寫完一個函數以後,都應該對這個函數的方方面面進行測試,這樣的測試咱們稱之爲單元測試。傳統的編程方式,進行單元測試是一件很麻煩的事情,你要 從新寫另一個程序,在該程序中調用你須要測試的方法,而且仔細觀察運行結果,看看是否有錯。正由於如此麻煩,因此程序員們編寫單元測試的熱情不是很高。 因而有一個牛人推出了單元測試包,大大簡化了進行單元測試所要作的工做,這就是JUnit4。本文簡要介紹一下在Eclipse3.2中使用JUnit4 進行單元測試的方法。
首先,咱們來一個傻瓜式速成教程,不要問爲何,Follow Me,先來體驗一下單元測試的快感!
首先新建一個項目叫JUnit_Test,咱們編寫一個Calculator類,這是一個可以簡單實現加減乘除、平方、開方的計算器類,而後對這些功能進行單元測試。這個類並非很完美,咱們故意保留了一些Bug用於演示,這些Bug在註釋中都有說明。該類代碼以下:
package andycpp;
public class Calculator …{
private static int result; // 靜態變量,用於存儲運行結果
public void add(int n) …{
result = result + n;
}
public void substract(int n) …{
result = result – 1; //Bug: 正確的應該是 result =result-n
}
public void multiply(int n) …{
} // 此方法還沒有寫好
public void divide(int n) …{
result = result / n;
}
public void square(int n) …{
result = n * n;
}
public void squareRoot(int n) …{
for (;;); //Bug : 死循環
}
public void clear() …{ // 將結果清零
result = 0;
}
public int getResult() …{
return result;
}
}
第二步,將JUnit4單元測試包引入這個項目:在該項目上點右鍵,點「屬性」,如圖:
在彈出的屬性窗口中,首先在左邊選擇「Java Build Path」,而後到右上選擇「Libraries」標籤,以後在最右邊點擊「Add Library…」按鈕,以下圖所示:
而後在新彈出的對話框中選擇JUnit4並點擊肯定,如上圖所示,JUnit4軟件包就被包含進咱們這個項目了。
第三步,生成JUnit測試框架:在Eclipse的Package Explorer中用右鍵點擊該類彈出菜單,選擇「New à JUnit Test Case」。以下圖所示:
在彈出的對話框中,進行相應的選擇,以下圖所示:
點擊「下一步」後,系統會自動列出你這個類中包含的方法,選擇你要進行測試的方法。此例中,咱們僅對「加、減、乘、除」四個方法進行測試。以下圖所示:
以後系統會自動生成一個新類CalculatorTest,裏面包含一些空的測試用例。你只須要將這些測試用例稍做修改便可使用。完整的CalculatorTest代碼以下:
package andycpp;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
public class CalculatorTest …{
private static Calculator calculator = new Calculator();
@Before
public void setUp() throws Exception …{
calculator.clear();
}
@Test
public void testAdd() …{
calculator.add(2);
calculator.add(3);
assertEquals(5, calculator.getResult());
}
@Test
public void testSubstract() …{
calculator.add(10);
calculator.substract(2);
assertEquals(8, calculator.getResult());
}
@Ignore(「Multiply() Not yet implemented」)
@Test
public void testMultiply() …{
}
@Test
public void testDivide() …{
calculator.add(8);
calculator.divide(2);
assertEquals(4, calculator.getResult());
}
}
第四步,運行測試代碼:按照上述代碼修改完畢後,咱們在CalculatorTest類上點右鍵,選擇「Run As à JUnit Test」來運行咱們的測試,以下圖所示:
運行結果以下:
進度條是紅顏色表示發現錯誤,具體的測試結果在進度條上面有表示「共進行了4個測試,其中1個測試被忽略,一個測試失敗」
至此,咱們已經完總體驗了在Eclipse中使用JUnit的方法。在接下來的文章中,我會詳細解釋測試代碼中的每個細節!
咱們繼續對初級篇中的例子進行分析。初級篇中咱們使用Eclipse自動生成了一個測試框架,在這篇文章中,咱們來仔細分析一下這個測試框架中的每個細節,知其然更要知其因此然,才能更加熟練地應用JUnit4。
1、包含必要地Package
在測試類中用到了JUnit4框架,天然要把 相應地Package包含進來。最主要地一個Package就是org.junit.*。把它包含進來以後,絕大部分功能就有了。還有一句話也很是地重要 「import static org.junit.Assert.*;」,咱們在測試的時候使用的一系列assertEquals方法就來自這個包。你們注意一下,這是一個靜態包含 (static),是JDK5中新增添的一個功能。也就是說,assertEquals是Assert類中的一系列的靜態方法,通常的使用方式是 Assert. assertEquals(),可是使用了靜態包含後,前面的類名就能夠省略了,使用起來更加的方便。
2、測試類的聲明
你們注意到,咱們的測試類是一個獨立的類,沒有任何父類。測試類的名字也能夠任意命名,沒有任何侷限性。因此咱們不能經過類的聲明來判斷它是否是一個測試類,它與普通類的區別在於它內部的方法的聲明,咱們接着會講到。
3、建立一個待測試的對象。
你要測試哪一個類,那麼你首先就要建立一個該類的對象。正如上一篇文章中的代碼:
private static Calculator calculator = new Calculator();
爲了測試Calculator類,咱們必須建立一個calculator對象。
4、測試方法的聲明
在測試類中,並非每個方法都是用於測試 的,你必須使用「標註」來明確代表哪些是測試方法。「標註」也是JDK5的一個新特性,用在此處很是恰當。咱們能夠看到,在某些方法的前有 @Before、@Test、@Ignore等字樣,這些就是標註,以一個「@」做爲開頭。這些標註都是JUnit4自定義的,熟練掌握這些標註的含義非 常重要。
5、編寫一個簡單的測試方法。
首先,你要在方法的前面使用@Test標註, 以代表這是一個測試方法。對於方法的聲明也有以下要求:名字能夠隨便取,沒有任何限制,可是返回值必須爲void,並且不能有任何參數。若是違反這些規 定,會在運行時拋出一個異常。至於方法內該寫些什麼,那就要看你須要測試些什麼了。好比:
@Test
public void testAdd() …{
calculator.add(2);
calculator.add(3);
assertEquals(5, calculator.getResult());
}
咱們想測試一下「加法」功能時候正確,就在測 試方法中調用幾回add函數,初始值爲0,先加2,再加3,咱們期待的結果應該是5。若是最終實際結果也是5,則說明add方法是正確的,反之說明它是錯 的。assertEquals(5, calculator.getResult());就是來判斷期待結果和實際結果是否相等,第一個參數填寫期待結果,第二個參數填寫實際結果,也就是經過 計算獲得的結果。這樣寫好以後,JUnit會自動進行測試並把測試結果反饋給用戶。
6、忽略測試某些還沒有完成的方法。
若是你在寫程序前作了很好的規劃,那麼哪些方 法是什麼功能都應該實現定下來。所以,即便該方法還沒有完成,他的具體功能也是肯定的,這也就意味着你能夠爲他編寫測試用例。可是,若是你已經把該方法的測 試用例寫完,但該方法還沒有完成,那麼測試的時候必定是「失敗」。這種失敗和真正的失敗是有區別的,所以JUnit提供了一種方法來區別他們,那就是在這種 測試函數的前面加上@Ignore標註,這個標註的含義就是「某些方法還沒有完成,暫不參與這次測試」。這樣的話測試結果就會提示你有幾個測試被忽略,而不 是失敗。一旦你完成了相應函數,只須要把@Ignore標註刪去,就能夠進行正常的測試。
7、Fixture(暫且翻譯爲「固定代碼段」)
Fixture的含義就是「在某些階段必然被 調用的代碼」。好比咱們上面的測試,因爲只聲明瞭一個Calculator對象,他的初始值是0,可是測試完加法操做後,他的值就不是0了;接下來測試減 法操做,就必然要考慮上次加法操做的結果。這絕對是一個很糟糕的設計!咱們很是但願每個測試都是獨立的,相互之間沒有任何耦合度。所以,咱們就頗有必要 在執行每個測試以前,對Calculator對象進行一個「復原」操做,以消除其餘測試形成的影響。所以,「在任何一個測試執行以前必須執行的代碼」就 是一個Fixture,咱們用@Before來標註它,如前面例子所示:
@Before
public void setUp() throws Exception …{
calculator.clear();
}
這裏不在須要@Test標註,由於這不是一個test,而是一個Fixture。同理,若是「在任何測試執行以後須要進行的收尾工做」也是一個Fixture,使用@After來標註。因爲本例比較簡單,沒有用到此功能。
1、高級Fixture
上一篇文章中咱們介紹了兩個Fixture標 注,分別是@Before和@After,咱們來看看他們是否適合完成以下功能:有一個類是負責對大文件(超過500兆)進行讀寫,他的每個方法都是對 文件進行操做。換句話說,在調用每個方法以前,咱們都要打開一個大文件並讀入文件內容,這絕對是一個很是耗費時間的操做。若是咱們使用@Before和 @After,那麼每次測試都要讀取一次文件,效率及其低下。這裏咱們所但願的是在全部測試一開始讀一次文件,全部測試結束以後釋放文件,而不是每次測試 都讀文件。JUnit的做者顯然也考慮到了這個問題,它給出了@BeforeClass 和 @AfterClass兩個Fixture來幫咱們實現這個功能。從名字上就能夠看出,用這兩個Fixture標註的函數,只在測試用例初始化時執行 @BeforeClass方法,當全部測試執行完畢以後,執行@AfterClass進行收尾工做。在這裏要注意一下,每一個測試類只能有一個方法被標註爲 @BeforeClass 或 @AfterClass,而且該方法必須是Public和Static的。
2、限時測試。
還記得我在初級篇中給出的例子嗎,那個求平方根的函數有Bug,是個死循環:
public void squareRoot(int n) …{
for (;;) ; //Bug : 死循環
}
若是測試的時候遇到死循環,你的臉上絕對不會 露出笑容。所以,對於那些邏輯很複雜,循環嵌套比較深的程序,頗有可能出現死循環,所以必定要採起一些預防措施。限時測試是一個很好的解決方案。咱們給這 些測試函數設定一個執行時間,超過了這個時間,他們就會被系統強行終止,而且系統還會向你彙報該函數結束的緣由是由於超時,這樣你就能夠發現這些Bug 了。要實現這一功能,只須要給@Test標註加一個參數便可,代碼以下:
@Test(timeout = 1000)
public void squareRoot() …{
calculator.squareRoot(4);
assertEquals(2, calculator.getResult());
}
Timeout參數代表了你要設定的時間,單位爲毫秒,所以1000就表明1秒。
3、 測試異常
JAVA中的異常處理也是一個重點,所以你經 常會編寫一些須要拋出異常的函數。那麼,若是你以爲一個函數應該拋出異常,可是它沒拋出,這算不算Bug呢?這固然是Bug,並JUnit也考慮到了這一 點,來幫助咱們找到這種Bug。例如,咱們寫的計算器類有除法功能,若是除數是一個0,那麼必然要拋出「除0異常」。所以,咱們頗有必要對這些進行測試。 代碼以下:
@Test(expected = ArithmeticException.class)
public void divideByZero() …{
calculator.divide(0);
}
如上述代碼所示,咱們須要使用@Test標註的expected屬性,將咱們要檢驗的異常傳遞給他,這樣JUnit框架就能自動幫咱們檢測是否拋出了咱們指定的異常。
4、 Runner (運行器)
你們有沒有想過這個問題,當你把測試代碼提交 給JUnit框架後,框架如何來運行你的代碼呢?答案就是——Runner。在JUnit中有不少個Runner,他們負責調用你的測試代碼,每個 Runner都有各自的特殊功能,你要根據須要選擇不一樣的Runner來運行你的測試代碼。可能你會以爲奇怪,前面咱們寫了那麼多測試,並無明確指定一 個Runner啊?這是由於JUnit中有一個默認Runner,若是你沒有指定,那麼系統自動使用默認Runner來運行你的代碼。換句話說,下面兩段 代碼含義是徹底同樣的:
import org.junit.internal.runners.TestClassRunner;
import org.junit.runner.RunWith;
//使用了系統默認的TestClassRunner,與下面代碼徹底同樣
public class CalculatorTest …{…}
@RunWith(TestClassRunner.class)
public class CalculatorTest …{…}
從上述例子能夠看出,要想指定一個 Runner,須要使用@RunWith標註,而且把你所指定的Runner做爲參數傳遞給它。另一個要注意的是,@RunWith是用來修飾類的,而 不是用來修飾函數的。只要對一個類指定了Runner,那麼這個類中的全部函數都被這個Runner來調用。最後,不要忘了包含相應的Package哦, 上面的例子對這一點寫的很清楚了。接下來,我會向大家展現其餘Runner的特有功能。
5、 參數化測試。
你可能遇到過這樣的函數,它的參數有許多特殊 值,或者說他的參數分爲不少個區域。好比,一個對考試分數進行評價的函數,返回值分別爲「優秀,良好,通常,及格,不及格」,所以你在編寫測試的時候,至 少要寫5個測試,把這5中狀況都包含了,這確實是一件很麻煩的事情。咱們還使用咱們先前的例子,測試一下「計算一個數的平方」這個函數,暫且分三類:正 數、0、負數。測試代碼以下:
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;
public class AdvancedTest …{
private static Calculator calculator = new Calculator();
@Before
public void clearCalculator() …{
calculator.clear();
}
@Test
public void square1() …{
calculator.square(2);
assertEquals(4, calculator.getResult());
}
@Test
public void square2() …{
calculator.square(0);
assertEquals(0, calculator.getResult());
}
@Test
public void square3() …{
calculator.square(-3);
assertEquals(9, calculator.getResult());
}
}
爲了簡化相似的測試,JUnit4提出了「參數化測試」的概念,只寫一個測試函數,把這若干種狀況做爲參數傳遞進去,一次性的完成測試。代碼以下:
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import java.util.Arrays;
import java.util.Collection;
@RunWith(Parameterized.class)
public class SquareTest …{
private static Calculator calculator = new Calculator();
private int param;
private int result;
@Parameters
public static Collection data() …{
return Arrays.asList(new Object[][]…{
…{2, 4},
…{0, 0},
…{-3, 9},
});
}
//構造函數,對變量進行初始化
public SquareTest(int param, int result) …{
this.param = param;
this.result = result;
}
@Test
public void square() …{
calculator.square(param);
assertEquals(result, calculator.getResult());
}
}
下面咱們對上述代碼進行分析。首先,你要爲這 種測試專門生成一個新的類,而不能與其餘測試共用同一個類,此例中咱們定義了一個SquareTest類。而後,你要爲這個類指定一個Runner,而不 能使用默認的Runner了,由於特殊的功能要用特殊的Runner嘛。@RunWith(Parameterized.class)這條語句就是爲這個 類指定了一個ParameterizedRunner。第二步,定義一個待測試的類,而且定義兩個變量,一個用於存放參數,一個用於存放期待的結果。接下 來,定義測試數據的集合,也就是上述的data()方法,該方法能夠任意命名,可是必須使用@Parameters標註進行修飾。這個方法的框架就不予解 釋了,你們只須要注意其中的數據,是一個二維數組,數據兩兩一組,每組中的這兩個數據,一個是參數,一個是你預期的結果。好比咱們的第一組{2, 4},2就是參數,4就是預期的結果。這兩個數據的順序無所謂,誰前誰後均可以。以後是構造函數,其功能就是對先前定義的兩個參數進行初始化。在這裏你可 要注意一下參數的順序了,要和上面的數據集合的順序保持一致。若是前面的順序是{參數,期待的結果},那麼你構造函數的順序也要是「構造函數(參數, 期待的結果)」,反之亦然。最後就是寫一個簡單的測試例了,和前面介紹過的寫法徹底同樣,在此就很少說。
6、 打包測試。
經過前面的介紹咱們能夠感受到,在一個項目 中,只寫一個測試類是不可能的,咱們會寫出不少不少個測試類。但是這些測試類必須一個一個的執行,也是比較麻煩的事情。鑑於此,JUnit爲咱們提供了打 包測試的功能,將全部須要運行的測試類集中起來,一次性的運行完畢,大大的方便了咱們的測試工做。具體代碼以下:
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses(…{CalculatorTest.class, SquareTest.class})
public class AllCalculatorTests …{}
你們能夠看到,這個功能也須要使用一個特殊的 Runner,所以咱們須要向@RunWith標註傳遞一個參數Suite.class。同時,咱們還須要另一個標註 @Suite.SuiteClasses,來代表這個類是一個打包測試類。咱們把須要打包的類做爲參數傳遞給該標註就能夠了。有了這兩個標註以後,就已經 完整的表達了全部的含義,所以下面的類已經可有可無,隨便起一個類名,內容所有爲空既可。