JUnit是一個開源的java自動化單元測試框架。由 Erich Gamma 和 Kent Beck 與1997年開發完成。 Erich Gamma 是 GOF的作者 之一;Kent Beck 則在 XP 中有重要的貢獻。Junit常常被開發者用來做單元測試的自動化測試框架。但是按照測試人員的角度來說,Junit更嚴格來講是一個測試驅動器,我們不僅僅可以用其來做單元測試,也是可以以其為核心來做集成測試、系統測試。很多人分析Junit喜歡從開發角度從設計模式來分析,但是對于測試人員來說需要的是從中學習自動化測試框架的基本組成部分與結構模式。學習Junit不僅僅是學習其用法,更要從中學習一個自動化測試框架的有哪些組成部分?各個組成部分之間是如何協調運作來形成一個可擴展穩健的自動化測試框架。Junit可謂“麻雀雖小,五臟俱全。”,其包含一個自動化測試框架所有的要素部分,很多的測試框架,千變萬化其實都離不開一個測試框架必有的組件與模式。本系列嘗試從一個手工測用例轉化為自動化執行以后的模式,來總結出一個自動化測試所必須的一個組成部分,并借用Junit來說明Junit中是如何實現這幾個部分的。
基本概述
讓我們從平時做測試的一個簡單情形開始。想想平時我們是如何做測試的,或者說是如何執行測試過程的?是不是有用例?我們編寫了用例,在用例里面說明了測試預置環境,測試輸出數據,執行步驟,和預期的結果。例如:
拿到這個用例,是不是要一步步按照用例的說明來操作。在人工測試的情況下類似如下的情況,
在這個過程當中,測試人員是執行者,讀取測試用例內容,操作被測對象,并判斷結果是否正確。
那么,如果要把上述的過程自動化需要如何做呢?在自動化的過程中,人不在了,那么誰來讀取測試用例呢?準確點說由誰來讀取測試用例的文件,并逐條執行里面的用例呢?首先想到就是寫一個程序,此程序能讀取文件,并識別里面的逐條的測試用例,然后執行。這樣的一個程序叫做測試執行器(Test Runner)。
有了執行器以后,又面臨一個問題,在人工操過程中,人是可以識別文件里的文本用例,讀懂它的步驟然后操作,但執行器就不行了,它是程序他無法識別文本的意思。程序識別程序,那么自然而然想到的方法就是把測試用例方法化,即把測試用例用程序的方法來表示,即測試方法(Test Method),因此執行器執行用例,就轉化為執行器調用方法的過程。
我們進一步觀察,測試方法,實時上,一個方法()Test Method 就代表一個測試用例,一個測試用例主要包含的重要信息,就是用例的執行環境是什么,執行步驟,預期值是什么,等等。類似的測試方法也需要包含四個步驟,稱為測試四步驟(Four-Phase Test)
- 環境設置
- SUT調用
- 結果驗證
- 環境清理
回顧之前所述,在測當中一個重要的步驟就是驗證,手動測試中,肉眼查看運行結果并和預期對比來得出測試是否通過的結果。在自動化過程中自然需要這個過程,讓測試方法自動驗證測試結果呢?這部自動化測試中稱為斷言(Assert)。
在整個的測試過程當中還包過如何過濾測試方法、、測試數據管理、最終測試結果報告等等。
如上述,我們總結一下,在自動化測試當中需要涉及的部分包括:
- 測試執行器(Runner):讀取測試用例、執行測試用例、測試報告輸出
- 測試輸入數據管理:如果測試需要外部輸入數據,這些數據如何組織管理
- 環境設置(setUp tearDown):主要設置用例執行前的環境、測試結束后清理測試環境;
- 結果斷言(Assert):如何來判斷測試結果是否符合預期
- 測試組織與流程控制:如何來組織測試用例,測試用例的過程如何控制
那么作為一個自動化的測試框架,Junit如何對上述各個組成部分進行處理呢,我們將從一個Junti入門例子開始,先對Junit做大致介紹,然后逐一演示Junit是如何實現上述各個部分的?只要大家理解了Junit對上述幾個部分是怎么做的,后續學習其他測試驅動器就可以快速入手,比如TestNG-在講完Junit后,我們會針對TestNG如何實現上是問題做簡單說明,只要大家牢固掌握Junit實現上述問題的本質,學習testNG也就是分分鐘的事情。
在Eclipse中使用JUnit4
在開始之前我們先把Junit引入到我們的Eclipse的項目中來,在Eclipse項目右鍵選中,如下圖所示
在彈出的屬性窗口中,首先在左邊選擇“Java Build Path”,然后到右上選擇“Libraries”標簽,之后在最右邊點擊“Add Library…”按鈕,如下圖所示:
然后在新彈出的對話框中選擇JUnit4并點擊確定,如上圖所示,JUnit4軟件包就被包含進我們這個項目了。
Junit 入門例子
假設我們有一個計算器類,包含兩個方法,add和multiply。
public class Calculator {
public int add(int one, int another) {
return one + another;
}
public int multiply(int one, int another) {
return one * another;
}
}
對這兩個方法,我們寫兩條happy path的測試用例:
要把這兩條測試用例自動化執行,要涉及前面所述的幾個步驟,首先是要測試用例腳本化,把測試用例的步驟用代碼方法來表示,我們建一個類叫CalculatorTest,包含這兩條測試用例:
public class CalculatorTest {
public void testAdd(){
Calculator calculator = new Calculator();
int sum = calculator.add(6,7);
if(sum == 13) {
System.out.println("add() SUCCESS!");
} else {
System.out.println("add() FAIL!");
}
}
public void testMultiply(){
Calculator calculator = new Calculator();
int product = calculator.multiply(8,9);
if (product == 56) {
System.out.println("multiply() SUCCESS!");
} else {
System.out.println("multiply() FAILs!");
}
}
}
測試方法是不是要執行器來執行?如果我沒有Junit這樣的框架,我們怎么執行上述的測試方法呢?如下也許是一種方法:
public class Client {
public static void main(String[] args) {
CalculatorTest test = new CalculatorTest();
test.testAdd();
test.testMultiply();
}
}
然后再通過某種方式運行這個main 方法,查看打印的輸出,來驗證測試是成功還是失敗。想想一下,如果我們有很多的類,每個類都有很多方法,每個方法都要寫很多的分支來判斷結果,而且還要寫個main方法一個個去調用各個類方法,明顯是不可行呢?如果有一個程序--執行器,能夠自動創建一個測試類,并自動調用里面的各個測試方法那么就簡化很多了。Junit框架就提供了這樣的功能,Junit內置的執行器,能夠讀取一個類,并執行里面的測試方法,要讓執行者自動找到測試方法并調用,必須要有一定的約定,比如告訴執行器說用test開頭的方法就是測試方法(Junit 3所采用的方法)或者給測試方法某個標注來表示測試方法,那么執行器就會按著這個約定去調用類里的有特定命名方式或者有特定標志的測試方法。我們來Junit來改寫上述的測試:
public class CalculatorTest {
@Test
public void testAdd(){
Calculator calculator = new Calculator();
int sum = calculator.add(6,7);
if(sum == 13) {
System.out.println("add() SUCCESS!");
} else {
System.out.println("add() FAIL!");
}
}
@Test
public void testMultiply(){
Calculator calculator = new Calculator();
int product = calculator.multiply(8,9);
if (product == 56) {
System.out.println("multiply() SUCCESS!");
} else {
System.out.println("multiply() FAILs!");
}
}
}
大家注意到每個測試方多了一個注解@Test,這個注解的作用的就是告訴Junit的Test Runner 這是一個測試方法,Test Runner就會調用這個方法。這時候你就不用自己寫一個main來執行這些方法,在Eclipse下右鍵 Run as Junit Test,Junit的測試執行器就會自動運行里面的測試方法。
還有兩個地方:一個是設置部分,每個測試方法都創建了一個Calculator,這個重復的代碼是否可以抽取出來呢?還有判斷結果部分分支復雜,能否簡化了?Junit提供fixture和斷言Assert相關方案來解決這兩個問題,看再一次用Junit簡化后的測試代碼:
public class CalculatorTest {
// fixture部分
public Calculator calculator;
@Before
public void setUp(){
calculator = new Calculator();
}
//測試方法
@Test
public void testAdd(){
calculator = new Calculator();
int sum = calculator.add(6,7);
//斷言部分
Assert.assertEquals(13, sum);
}
@Test
public void testMultiply(){
calculator = new Calculator();
int product = calculator.multiply(8,9);
//斷言部分
Assert.assertEquals(72, product);
}
}
上述@Berfore 表示此方法在每個測試方法運行之前運行一次。Assert.assertEquals判斷實際結果與預期是否一致。至此,這就是一個簡單完整的Junit測試代碼。用Junit執行器運行后有如下圖的測試結果展示:
上述的代碼中有疑問的地方先不管,這只是讓大家體會一些一個Junit測試的整體流程,各個注解后面會有詳細介紹。這邊講一些測試的統一結構和命名問題。
統一的測試結構
回顧我們的第一個Junit測試,可以總結出一個良好的測試方法包含四個步驟,就是上回我們提到的:
- 設置:稱為Fixture
- 執行:掉測試方法
- 斷言:判斷結果是否預期
- 清理:清理測試,相關例子后續會看到
極力建議再寫測試過程中中應該在每個階段開始時做好注釋。
測試方法命名規則
Junit4之前,注解,Junit必須采用一種方法來區分普通的方法和測試方法,當時采用的辦法標識 以test為前綴的方法名、public、非static、void、無參的方法為測試方法,因此,測試方法名稱都testFoo,testBar諸如此類。
但是Junit設計師認為這種的命名方式有時候無法真實揭示測試方法真實的用意,當一個方法對應的測試方法多了以后,甚至還會造成一些混亂,因此在Junit采用@Test注解來標識測試方法后,Junit取消測試方法名稱必須test開頭的規定,而是可以隨意命名,但是大部分人還是以test開頭。
后面有人提出 行為表達式(Behavior-expressing patterns)的命名方式:
[UnitOfWork_StateUnderTest]
下劃線可以用with、if 等此替換。這種方式的作用是希望能讓測試用例和代碼文檔一樣易于閱讀,比如上述的代碼寫久了,回頭去看不知道方法testMultiply在乘積溢出情況下會返回什么,如果你的測試名稱是這么寫的:
multiplyOverflowWithException()
那么看測試方法的人就知道這測試方法,在溢出時會拋異常
集體怎么命名取決與你們的團隊是如何約定,看個人喜好。
總結
我們討論手工測試自動化后,需要處理的幾個步驟,包括測試方法的讀取與執行、測試執行環境的設置、測試結果的判斷、測試報告、測試數據的管理、測試用例的組織等方面,一般的自動化測試框架要解決也就這幾個方面,對于Junit testNg無不如此。對于Junit 我們將從測試執行、斷言、執行器、異常測試、測試組織等幾個大方面詳細介紹它的使用