首先我們需要先下載相應(yīng)的 JUnit 相關(guān)的 JAR 包,下載的過(guò)程可以去 JUnit 的官方網(wǎng)站,也可以直接通過(guò) Maven 資源倉(cāng)庫(kù)來(lái)完成,我這里直接通過(guò)開(kāi)源中國(guó)社區(qū)在國(guó)內(nèi)的Maven 鏡像下載了 JUnit-4.8.2.jar 的版本,如下圖所示:
直接搜索關(guān)鍵字"junit"即可,我們可以從搜索結(jié)果中找到紅色方框1中的對(duì)應(yīng)項(xiàng),選中之后就會(huì)在左下方列出目前可以獲得的 junit 的所有版本,這里我選擇了 4.8.2 的版本(紅色方框2),然后點(diǎn)擊右側(cè)的 Download 按鈕即可下載這個(gè) JAR 包,不過(guò)依我在實(shí)踐中的經(jīng)驗(yàn),最好同時(shí)下載對(duì)應(yīng)版本的 javadoc 和 source 兩個(gè)包,前者是文檔,后者是對(duì)應(yīng)的源代碼,然后將它們直接引入到我們的 eclipse 的工程中即可。我這里創(chuàng)建了壹個(gè)普通的
Java Project ,并給它取名 junit-study ,導(dǎo)入 JAR 包之后目前的樣子如下圖所示:
使用簡(jiǎn)單的 @Test 注解實(shí)現(xiàn)我們的
測(cè)試方法的編寫(xiě)和執(zhí)行
準(zhǔn)備
工作做好之后,接下來(lái)我們就可以開(kāi)始嘗試編寫(xiě)壹個(gè)簡(jiǎn)單的測(cè)試代碼了。首先,我們編寫(xiě)了壹個(gè) Calculator 類,并提供五個(gè)方法分別完成加減乘除以及求平方的運(yùn)算。代碼如下:
package net.oschina.bairrfhoinn.main; public class Calculator { public void add(int n){ result += n; } public void substract(int n){ result -= n; } public void multiply(int n){ result *= n; } public void divide(int n){ result /= n; } public void square(int n){ result = n * n; } public int getReuslt(){ return result; } public void clear(){ result = 0; } private static int result; } |
在測(cè)試類中用到了JUnit4框架,自然要把相應(yīng)地Package包含進(jìn)來(lái)。最主要地一個(gè)Package就是org.junit.*。把它包含進(jìn)來(lái)之后,絕大部分功能就有了。還有一句話也非常地重要“import static org.junit.Assert.*;”,我們?cè)跍y(cè)試的時(shí)候使用的壹系列assertEquals()方法就來(lái)自這個(gè)包。大家注意壹下,這是壹個(gè)靜態(tài)包含(static),是JDK5中新增添的壹個(gè)功能。也就是說(shuō),assertEquals是Assert類中的壹系列的靜態(tài)方法,壹般的使用方式是Assert. assertEquals(),但是使用了靜態(tài)包含后,前面的類名就可以省略了,使用起來(lái)更加的方便。
另外要注意的是,我們的測(cè)試類是壹個(gè)獨(dú)立的類,沒(méi)有任何父類。測(cè)試類的名字也可以任意命名,沒(méi)有任何局限性。所以我們不能通過(guò)類的聲明來(lái)判斷它是不是一個(gè)測(cè)試類,它與普通類的區(qū)別在于它內(nèi)部的方法的聲明,我們接著會(huì)講到。在測(cè)試類中,并不是每壹個(gè)方法都是用于測(cè)試的,所以我們必須使用“注解”來(lái)明確表明哪些是測(cè)試方法。“注解”也是JDK5的壹個(gè)新特性,用在此處非常恰當(dāng)。我們可以看到,在某些方法的前有@Before、@Test、@Ignore等字樣,這些就是注解,以壹個(gè)“@”作為開(kāi)頭。這些注解都是JUnit4自定義的,熟練掌握這些注解的含義,對(duì)于編寫(xiě)恰當(dāng)?shù)臏y(cè)試類非常重要。
接下來(lái)我們創(chuàng)建壹個(gè)測(cè)試類 CalculatorTest.java,代碼如下:
package net.oschina.bairrfhoinn.test; import static org.junit.Assert.*; import org.junit.Test; import net.oschina.bairrfhoinn.main.Calculator; public class CalculatorTest { private static Calculator calculator = new Calculator(); @Test public void testAdd(){ calculator.add(7); calculator.add(8); assertEquals(15, calculator.getReuslt()); } } |
首先,我們要在方法的前面使用@Test標(biāo)注,以表明這是壹個(gè)測(cè)試方法。對(duì)于方法的聲明也有如下要求:名字可以隨便取,沒(méi)有任何限制,但是返回值必須為void,而且不能有任何參數(shù)。如果違反這些規(guī)定,會(huì)在運(yùn)行時(shí)拋出壹個(gè)異常。至于方法內(nèi)該寫(xiě)些什么,那就要看你需要測(cè)試些什么了。比如上述代碼中,我們想測(cè)試壹下add()方法的功能是否正確,就在測(cè)試方法中調(diào)用幾次add函數(shù),初始值為0,先加7,再加8,我們期待的結(jié)果應(yīng)該是15。如果最終實(shí)際結(jié)果也是15,則說(shuō)明add()方法是正確的,反之說(shuō)明它是錯(cuò)的。assertEquals(15, calculator.getResult());就是用來(lái)判斷期待結(jié)果和實(shí)際結(jié)果是否相等,其中第壹個(gè)參數(shù)填寫(xiě)期待結(jié)果,第二個(gè)參數(shù)填寫(xiě)實(shí)際結(jié)果,也就是通過(guò)計(jì)算得到的結(jié)果。這樣寫(xiě)好之后,JUnit 會(huì)自動(dòng)進(jìn)行測(cè)試并把測(cè)試結(jié)果反饋給用戶。
如果想運(yùn)行它,可以在 eclipse 的資源管理器中選擇該類文件,然后點(diǎn)擊右鍵,選擇 Run As->JUnit Test 即可看到運(yùn)行結(jié)果如下圖所示:
使用@Test 的屬性 Ignore 指定測(cè)試時(shí)跳過(guò)這個(gè)方法
如果你在寫(xiě)程序前做了很好的規(guī)劃,那么哪些方法是什么功能都應(yīng)該實(shí)現(xiàn)并且確定下來(lái)。因此,即使該方法尚未完成,他的具體功能也是確定的,這也就意味著你可以為他編寫(xiě)測(cè)試用例。但是,如果你已經(jīng)把該方法的測(cè)試用例寫(xiě)完,但該方法尚未完成,那么測(cè)試的時(shí)候無(wú)疑是“失敗”。這種失敗和真正的失敗是有區(qū)別的,因此 JUnit 提供了壹種方法來(lái)區(qū)別他們,那就是在這種測(cè)試函數(shù)的前面加上 @Ignore 標(biāo)注,這個(gè)標(biāo)注的含義就是“某些方法尚未完成,暫不參與此次測(cè)試”。這樣的話測(cè)試結(jié)果就會(huì)提示你有幾個(gè)測(cè)試被忽略,而不是失敗。壹旦你完成了相應(yīng)函數(shù),只需要把@Ignore標(biāo)注刪去,就可以進(jìn)行正常的測(cè)試。
比如說(shuō)上面的測(cè)試類 Calculator.java 中,假設(shè)我們的 Calculator 類的 multiply() 方法沒(méi)有實(shí)現(xiàn),我們可以在測(cè)試類 CalculatorTest 中先寫(xiě)如下測(cè)試代碼:
package net.oschina.bairrfhoinn.test; import static org.junit.Assert.*; import org.junit.Ignore; import org.junit.Test; import net.oschina.bairrfhoinn.main.Calculator; public class CalculatorTest { private static Calculator calculator = new Calculator(); ... //此處代碼省略 @Ignore("method square() not implemented, please test this later...") @Test public void testSquare(){ calculator.square(3); assertEquals(9, calculator.getReuslt()); } } |
我們?cè)龠\(yùn)行壹次測(cè)試,會(huì)看到如下結(jié)果,從圖中可以很明顯的看出,方法testSquare() 上的 @Ignore 注解已經(jīng)生效了,運(yùn)行時(shí)直接跳過(guò)了它,而方法testAdd()仍然正常的運(yùn)行并通過(guò)了測(cè)試。
使用注解 @Before 和 @After 來(lái)完成前置工作和后置工作
前置工作通常是指我們的測(cè)試方法在運(yùn)行之前需要做的壹些準(zhǔn)備工作,如數(shù)據(jù)庫(kù)的連接、文件的加載、輸入數(shù)據(jù)的準(zhǔn)備等需要在運(yùn)行測(cè)試方法之前做的事情,都屬于前置工作;類似的,后置工作則是指測(cè)試方法在運(yùn)行之后的壹些要做的事情,如釋放數(shù)據(jù)庫(kù)連接、輸入輸出流的關(guān)閉等;比如我們上面的測(cè)試,由于只聲明了壹個(gè) Calculator 對(duì)象,他的初始值是0,但是測(cè)試完加法操作后,他的值就不是0了;接下來(lái)測(cè)試減法操作,就必然要考慮上次加法操作的結(jié)果。這絕對(duì)是壹個(gè)很糟糕的設(shè)計(jì)!我們非常希望每壹個(gè)測(cè)試方法都是獨(dú)立的,相互之間沒(méi)有任何耦合度。因此,我們就很有必要在執(zhí)行每壹個(gè)測(cè)試方法之前,對(duì)Calculator對(duì)象進(jìn)行壹個(gè)“復(fù)原”操作,以消除其他測(cè)試造成的影響。因此,“在任何壹個(gè)測(cè)試方法執(zhí)行之前必須執(zhí)行的代碼”就是壹個(gè)前置工作,我們用注解 @Before 來(lái)標(biāo)注它,如下例子所示:
package net.oschina.bairrfhoinn.test; ... import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; public class CalculatorTest { ...//這里省略部分代碼 @Before public void setUp() throws Exception { calculator.clear(); } @After public void tearDown() throws Exception { System.out.println("will do sth here..."); } ...//這里省略部分代碼 } |
另外要說(shuō)的是,注解 @Before 是定義在 org.junit.Before 這個(gè)類中的,因此使用時(shí)需要將其引入我們的代碼中。這樣做了之后,每次我們運(yùn)行測(cè)試方法時(shí),JUnit 都會(huì)先運(yùn)行 setUp() 方法將 result 的值清零。不過(guò)要注意的是,這里不再需要 @Test 注解,因?yàn)檫@并不是壹個(gè) test,只是壹個(gè)前置工作。同理,如果“在任何測(cè)試執(zhí)行之后需要進(jìn)行的收尾工作,我們應(yīng)該使用 @After 來(lái)標(biāo)注,方法與它類似。由于本例比較簡(jiǎn)單,不需要用到此功能,所以我們只是簡(jiǎn)單了給它添加了壹個(gè) tearDown() 方法并在收尾時(shí)打印壹句話到控制臺(tái),并且使用 @After 來(lái)注解這個(gè)方法。
使用@BeforeClass 和 @AfterClass 來(lái)完成只需要執(zhí)行壹次的前置工作和后置工作
上面我們提到了兩個(gè)注解 @Before 和 @After ,我們來(lái)看看他們是否適合完成如下功能:有壹個(gè)類負(fù)責(zé)對(duì)大文件(超過(guò)500 MB)進(jìn)行讀寫(xiě),他的每壹個(gè)方法都是對(duì)文件進(jìn)行操作。換句話說(shuō),在調(diào)用每壹個(gè)方法之前,我們都要打開(kāi)壹個(gè)大文件并讀入文件內(nèi)容,這絕對(duì)是壹個(gè)非常耗費(fèi)時(shí)的操作。如果我們使用 @Before 和 @After ,那么每次測(cè)試都要讀取壹次文件,效率及其低下。所以我們希望的是,在所有測(cè)試壹開(kāi)始讀壹次文件,所有測(cè)試結(jié)束之后釋放文件,而不是每次測(cè)試都讀文件。JUnit的作者顯然也考慮到了這個(gè)問(wèn)題,它給出了@BeforeClass 和 @AfterClass 兩個(gè)注解來(lái)幫我們實(shí)現(xiàn)這個(gè)功能。從名字上就可以看出,用這兩個(gè)注解標(biāo)注的函數(shù),只在測(cè)試用例初始化時(shí)執(zhí)行 @BeforeClass 方法,當(dāng)所有測(cè)試執(zhí)行完畢之后,執(zhí)行 @AfterClass 進(jìn)行收尾工作。在這里要注意壹下,每個(gè)測(cè)試類只能有壹個(gè)方法被標(biāo)注為 @BeforeClass 或 @AfterClass,而且該方法必須是 public static 類型的。
使用@Test 的屬性 timeout 來(lái)完成限時(shí)測(cè)試,以檢測(cè)代碼中的死循環(huán)
現(xiàn)在假設(shè)我們的 Calculator 類中的 square() 方法是個(gè)死循環(huán),那應(yīng)該怎么辦呢,比如說(shuō)像下面這樣:
public void square(int n){ for(;;){} } |
如果測(cè)試的時(shí)候遇到死循環(huán),你的臉上絕對(duì)不會(huì)露出笑容的。因此,對(duì)于那些邏輯很復(fù)雜,循環(huán)嵌套比較深的、有可能出現(xiàn)死循環(huán)的程序,因此壹定要采取壹些預(yù)防措施。限時(shí)測(cè)試是壹個(gè)很好的解決方案。我們給這些測(cè)試函數(shù)設(shè)定壹個(gè)預(yù)期的執(zhí)行時(shí)間,超過(guò)了這壹時(shí)間,他們就會(huì)被系統(tǒng)強(qiáng)行終止,并且系統(tǒng)還會(huì)向你匯報(bào)該函數(shù)結(jié)束的原因是因?yàn)槌瑫r(shí),這樣你就可以發(fā)現(xiàn)這些 Bug 了。要實(shí)現(xiàn)這壹功能,只需要給 @Test 標(biāo)注加壹個(gè)參數(shù)timeout即可,代碼如下:
@Test(timeout=2000L) public void testSquare() { calculator.square(3); assertEquals(9, calculator.getReuslt()); } |
timeout參數(shù)表明了你預(yù)計(jì)該方法運(yùn)行的時(shí)長(zhǎng),單位為毫秒,因此2000就代表2秒。現(xiàn)在我們讓這個(gè)測(cè)試方法運(yùn)行壹下,看看失敗時(shí)是什么效果。
使用@Test 的屬性expected來(lái)監(jiān)控測(cè)試方法中可能會(huì)拋出的某些異常
JAVA中的異常處理也是壹個(gè)重點(diǎn),因此你經(jīng)常會(huì)編寫(xiě)壹些需要拋出異常的函數(shù)。如果你覺(jué)得壹個(gè)函數(shù)應(yīng)該拋出異常,但是它沒(méi)拋出,這算不算 Bug 呢?這當(dāng)然是Bug,JUnit 也考慮到了這壹點(diǎn),并且可以幫助我們找到這種 Bug。例如,我們寫(xiě)的計(jì)算器類有除法功能,如果除數(shù)是壹個(gè)0,那么必然要拋出“除0異常”。因此,我們很有必要對(duì)這些進(jìn)行測(cè)試。代碼如下:
@Test(expected=java.lang.ArithmeticException.class) public void testDivide(){ calculator.divide(0); } |
如上述代碼所示,我們需要使用@Test注解中的expected屬性,將我們要檢驗(yàn)的異常(這里是 java.lang.ArithmeticException)傳遞給他,這樣 JUnit 框架就能自動(dòng)幫我們檢測(cè)是否拋出了我們指定的異常。
指定 JUnit 運(yùn)行測(cè)試用例時(shí)的 Runner
大家有沒(méi)有想過(guò)這個(gè)問(wèn)題,當(dāng)你把測(cè)試代碼提交給JUnit框架后,框架是如何來(lái)運(yùn)行你的代碼的呢?答案就是Runner。在JUnit中有很多個(gè)Runner,他們負(fù)責(zé)調(diào)用你的測(cè)試代碼,每壹個(gè)Runner都有其各自的特殊功能,你要根據(jù)需要選擇不同的Runner來(lái)運(yùn)行你的測(cè)試代碼。可能你會(huì)覺(jué)得奇怪,前面我們寫(xiě)了那么多測(cè)試,并沒(méi)有明確指定壹個(gè)Runner啊?這是因?yàn)镴Unit中有壹個(gè)默認(rèn)的Runner,如果你沒(méi)有指定,那么系統(tǒng)會(huì)自動(dòng)使用默認(rèn)Runner來(lái)運(yùn)行你的代碼。換句話說(shuō),下面兩段代碼含義是完全壹樣的:
import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class CalculatorTest { ...//省略此處代碼 } //用了系統(tǒng)默認(rèn)的JUnit4.class,運(yùn)行效果完全壹樣 public class CalculatorTest { ...//省略此處代碼 } |
從上述例子可以看出:
1、要想指定壹個(gè) Runner ,需要使用 @RunWith 標(biāo)注,并且把你所指定的 Runner 類名作為參數(shù)傳遞給它,在JUnit4.8.2的版本中,系統(tǒng)提供了若干可以直接使用的Runner類型,它們的定義都在包org.junit.runners下面。
2、注解 @RunWith 是用來(lái)修飾類的,而不是用來(lái)修飾函數(shù)的。只要對(duì)壹個(gè)類指定了 Runner ,那么這個(gè)類中的所有函數(shù)都被這個(gè) Runner 來(lái)調(diào)用。
3、在使用注解@RunWith時(shí),要在頭部包含相應(yīng)的包名,上面的例子對(duì)這壹點(diǎn)寫(xiě)的很清楚了。
接下來(lái),我會(huì)向你們展示其他 Runner 的特有功能。
使用參數(shù)化測(cè)試完成需要錄入大量數(shù)據(jù)的測(cè)試
你可能遇到過(guò)這樣的函數(shù),它的參數(shù)有許多特殊值,或者說(shuō)他的參數(shù)分為很多個(gè)區(qū)域。比如,壹個(gè)對(duì)考試分?jǐn)?shù)進(jìn)行評(píng)價(jià)的函數(shù),返回值分別為“優(yōu)秀,良好,壹般,及格,不及格”,因此你在編寫(xiě)測(cè)試的時(shí)候,至少要寫(xiě)5個(gè)測(cè)試,把這五種情況都包含了,這確實(shí)是壹件很麻煩的事情。這里我們?nèi)匀皇褂孟惹暗睦樱瑴y(cè)試壹下square()這個(gè)函數(shù),暫且分三類:正數(shù)、0、負(fù)數(shù)。測(cè)試代碼如下:
package net.oschina.bairrfhoinn.test; import static org.junit.Assert.*; import net.oschina.bairrfhoinn.main.Calculator; import org.junit.Before; import org.junit.Test; public class AdvancedTest { private static Calculator calculator = new Calculator(); @Before public void setUp() throws Exception { calculator.clear(); } @Test public void testSquare1(){ calculator.square(2); assertEquals(4, calculator.getReuslt()); } @Test public void testSquare2(){ calculator.square(0); assertEquals(0, calculator.getReuslt()); } @Test public void testSquare3(){ calculator.square(-3); assertEquals(9, calculator.getReuslt()); } } |
為了簡(jiǎn)化類似的測(cè)試,JUnit4提出了“參數(shù)化測(cè)試”的概念,只寫(xiě)壹個(gè)測(cè)試函數(shù),把這若干種情況的輸入?yún)?shù)和預(yù)期的運(yùn)行結(jié)果放在集合中,然后將這個(gè)集合作為參數(shù)傳遞進(jìn)去,壹次性的完成測(cè)試。代碼如下:
package net.oschina.bairrfhoinn.test; import static org.junit.Assert.*; import java.util.Arrays; import java.util.Collection; import net.oschina.bairrfhoinn.main.Calculator; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) public class SquareTest { @Parameters public static Collection prepareData(){ 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.getReuslt()); } private int param; private int result; private static Calculator calculator = new Calculator(); } |