我們寫單元測試,一般都會用到一個或多個單元測試框架,在這裡,我們介紹一下JUnit4這個測試框架。這是Java界用的最廣泛,也是最基礎的一個框架,其他的很多框架,包括我們後面會看到的Robolectric,都是基於或相容JUnit4的。
然而首先要解決的問題是。。。
為什麼要使用單元測試框架
或者換句話說,單元測試框架能夠為我們做什麼呢?
從最基本的開始說起,假如我們有這樣一個類:
1 2 3 4 5 6 7 8 9 10 11 |
public class Calculator { public int add(int one, int another) { // 為了簡單起見,暫不考慮溢位等情況。 return one + another; } public int multiply(int one, int another) { // 為了簡單起見,暫不考慮溢位等情況。 return one * another; } } |
如果不用單元測試框架的話,我們要怎麼寫測試程式碼呢?我們恐怕得寫出下面這樣的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class CalculatorTest { public static void main(String[] args) { Calculator calculator = new Calculator(); int sum = calculator.add(1, 2); if(sum == 3) { System.out.println("add() works!") } else { System.out.println("add() does not works!") } int product = calculator.multiply(2, 4); if (product == 8) { System.out.println("multiply() works!") } else { System.out.println("multiply() does not works!") } } } |
然後我們再通過某種方式,比如命令列或IDE,執行這個CalculatorTest
的main
方法,在看著terminal的輸出,才知道測試是通過還是失敗。想想一下,如果我們有很多的類,每個類都有很多方法,那麼就要寫一堆這樣的程式碼,每個類對於一個含有main
方法的test類,同時main
方法裡面會有一堆程式碼。這樣既寫起來痛苦,跑起來更痛苦,比如說,你怎麼樣一次性跑所有的測試類呢?所以,一個測試框架為我們做的最基本的事情,就是允許我們按照某種更簡單的方式寫測試程式碼,把每一個測試單元寫在一個測試方法裡面,然後它會自動找出所有的測試方法,並且根據你的需要,執行所有的測試方法,或者是執行單個測試方法,或者是執行部分測試方法等等。 對於上面的Calculator
例子,如果使用Junit的話,我們可以按照如下的方式寫測試程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class CalculatorTest { public void testAdd() throws Exception { Calculator calculator = new Calculator(); int sum = calculator.add(1, 2); Assert.assertEquals(3, sum); } public void testMultiply() throws Exception { Calculator calculator = new Calculator(); int product = calculator.multiply(2, 4); Assert.assertEquals(8, product); } } |
每一個被測試的方法(add(), multiply()
),寫一個對應的測試方法(testAdd(), testMultiply()
)。那JUnit怎麼知道那些是測試方法,哪些不是呢?這個是通過前面的@Test
註解來標誌的,只要有這個註解,JUnit4就會當做是一個測試方法,方法名其實是可以隨意起的。當然,名字還是應該起的更有可讀性一點,讓人一看就知道,這個測試方法是測試了被測的類的那個方法,或者是測試了那個功能點等等。
除了幫我們找出所有的測試方法,並且方便執行意外,單元測試框架還幫我們做了其他事情。在這個系列的第一篇文章中我們提到,一個測試方法主要包括三個部分:
- setup
- 執行操作
- 驗證結果
而一個單元測試框架,可以讓我們更方便的寫上面的每一步的程式碼,尤其是第一步和第三部。比如說,在上面的CalculatorTest
中,testAdd()
和testMultiply()
都有相同的setup: Calculator calculator = new Calculator();
,如果Calculator
還有其他的方法的話,這行程式碼就得重複更多次,這種duplication是沒必要的。絕大多數單元測試框架考慮到了這一點,它們知道一個測試類的很多測試方法可能需要相同的setup,所以為我們提供了便捷方法。對於JUnit4,是通過@Before
來實現的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class CalculatorTest { Calculator mCalculator; @Before public void setup() { mCalculator = new Calculator(); } <a href='http://www.jobbole.com/members/weibolt100186'>@T</a> est public void testAdd() throws Exception { int sum = mCalculator.add(1, 2); assertEquals(3, sum); //為了簡潔,往往會static import Assert裡面的所有方法。 } <a href='http://www.jobbole.com/members/weibolt100186'>@T</a> est public void testMultiply() throws Exception { int product = mCalculator.multiply(2, 4); assertEquals(8, product); } } |
如果一個方法被@Before
修飾過了,那麼在每個測試方法呼叫之前,這個方法都會得到呼叫。所以上面的例子中,testAdd()
被執行之前,setup()
會被呼叫一次,把mCalculator
例項化,接著執行testAdd()
;testMultiply()
被執行之前,setup()
又會被呼叫一次,把mCalculator
再次例項化,接著執行testMultiply()
。如果還有其他的測試方法,則以此類推。
對應於@Before
的,有一個@After
,作用估計你也猜得到,那就是每個測試方法執行結束之後,會得到執行的方法。比如一個測試檔案操作的類,那麼在它的測試類中,可能@Before
裡面需要去開啟一個檔案,而每個測試方法執行結束之後,都需要去close這個檔案。這個時候就可以把檔案close的操作放在@After
裡面,讓它自動去執行。
類似的,還有@BeforeClass
和@AfterClass
。@BeforeClass
的作用是,在跑一個測試類的所有測試方法之前,會執行一次被@BeforeClass
修飾的方法,執行完所有測試方法之後,會執行一遍被@AfterClass
修飾的方法。這兩個方法可以用來setup和release一些公共的資源,需要注意的是,被這兩個annotation修飾的方法必須是靜態的。
前面講的是單元測試框架對於一個測試方法的第一步“setup”,為我們做的事情。而對於第三部“驗證結果”,則一般是通過一些assert方法來完成的。JUnit為我們提供的assert方法,多數都在Assert
這個類裡面。最常用的那些如下:
assertEquals(expected, actual)
驗證expected的值跟actual是一樣的,如果是一樣的話,測試通過,不然的話,測試失敗。如果傳入的是object,那麼這裡的對比用的是equals()
assertEquals(expected, actual, tolerance)
這裡傳入的expected和actual是float或double型別的,大家知道計算機表示浮點型資料都有一定的偏差,所以哪怕理論上他們是相等的,但是用計算機表示出來則可能不是,所以這裡執行傳入一個偏差值。如果兩個數的差異在這個偏差值之內,則測試通過,否者測試失敗。
assertTrue(boolean condition)
驗證contidion的值是true
assertFalse(boolean condition)
驗證contidion的值是false
assertNull(Object obj)
驗證obj的值是null
assertNotNull(Object obj)
驗證obj的值不是null
assertSame(expected, actual)
驗證expected和actual是同一個物件,即指向同一個物件
assertNotSame(expected, actual)
驗證expected和actual不是同一個物件,即指向不同的物件
fail()
讓測試方法失敗
注意:上面的每一個方法,都有一個過載的方法,可以在前面加一個String型別的引數,表示如果驗證失敗的話,將用這個字串作為失敗的結果報告。
比如:
assertEquals("Current user Id should be 1", 1, currentUser.id());
當currentUser.id()
的值不是1的時候,在結果報導里面將顯示”Current user Id should be 1″,這樣可以讓測試結果更具有可讀性,更清楚錯誤的原因是什麼。
比較有意思的是最後一個方法,fail()
,你或許會好奇,這個有什麼用呢?其實這個在很多情況下還是有用的,比如最明顯的一個作用就是,你可以驗證你的測試程式碼真的是跑了的。
此外,它還有另外一個重要作用,那就是驗證某個被測試的方法會正確的丟擲異常,不過這點可以通過下面講到的方法,更方便的做到,所以就不講了。
這部分相對來說還是很好理解的,不做過多解釋。
JUnit的其他功能
Ignore一些測試方法
很多時候,因為某些原因(比如正式程式碼還沒有實現等),我們可能想讓JUnit忽略某些方法,讓它在跑所有測試方法的時候不要跑這個測試方法。要達到這個目的也很簡單,只需要在要被忽略的測試方法前面加上@Ignore
就可以了,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class CalculatorTest { Calculator mCalculator; @Before public void setup() { mCalculator = new Calculator(); } // Omit testAdd() and testMultiply() for brevity <a href='http://www.jobbole.com/members/weibolt100186'>@T</a> est @Ignore("not implemented yet") public void testFactorial() { } } |
驗證方法會丟擲某些異常
有的時候,丟擲異常是一個方法正確工作的一部分。比如一個除法函式,當除數是0的時候,它應該丟擲異常,告訴外界,傳入的被除數是0,示例程式碼如下:
1 2 3 4 5 6 7 8 9 |
public class Calculator { // Omit testAdd() and testMultiply() for brevity public double divide(double divident, double dividor) { if (dividor == 0) throw new IllegalArgumentException("Dividor cannot be 0"); return divident / dividor; }} |
那麼如何測試當傳入的除數是0的時候,這個方法應該丟擲IllegalArgumentException
異常呢?
在Junit中,可以通過給@Test
annotation傳入一個expected引數來達到這個目的,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class CalculatorTest { Calculator mCalculator; @Before public void setup() { mCalculator = new Calculator(); } // Omit testAdd() and testMultiply() for brevity @Test(expected = IllegalArgumentException.class) public void test() { mCalculator.divide(4, 0); } } |
@Test(expected = IllegalArgumentException.class)
表示驗證這個測試方法將丟擲IllegalArgumentException
異常,如果沒有丟擲的話,則測試失敗。
在Android專案裡面使用JUnit
在Android專案裡面使用JUnit是很簡單的,你只需要將JUnit這個library加到你的dependencies裡面。
testCompile 'junit:junit:4.12'
如果你通過AndroidStudio建立一個專案,這個dependency預設是加上了的,所以你甚至這步都可以省略。
此外,你需要把測試程式碼放到src/test/java 目錄下面。
接下來關於怎麼樣執行測試程式碼,怎麼樣看結果,請參考這個系列的第一篇文章的相關部分,因為圖比較多,這邊就不重複了。
這裡讓大家看一下執行的結果是什麼樣子的,其中有一個失敗的測試用例是故意的。如果你直接在AndroidStudio裡面跑上面的測試類CalculatorTest的所有測試方法的話,會看到如下的結果:
左邊可以看到所有的測試方法,以及每個方法跑出來的結果,綠色表示測試通過的測試方法,黃色的感嘆號或紅色的表示測試失敗的。第三個那個有條紋的球球表示被忽略的測試方法。
如果是通過terminal跑的話,則會看到如下的測試結果:
這篇文章的相關程式碼可以在github的這個project看到。
小結
這篇文字大概簡單介紹了JUnit的使用,相對來說是比較簡單,也是比較容易理解的,希望能幫助到大家。其中Assert部分,可以幫我們驗證一個方法的返回結果。然而,這些只能幫我們測試有返回值的那些方法。在第一篇文章裡面我們講了,一個類的方法分兩種,一是有返回值的方法,這些可以通過我們今天講的JUnit來做測試。而另外一種沒有返回值的方法,即void方法,則要通過另外一個框架,Mockito,來驗證它的正確性。至於怎麼樣驗證void方法的正確性,以及Mockito的使用,請關注下一篇文章。
參考:
http://junit.org/junit4/
http://www.vogella.com/tutorials/JUnit/article.html
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!