1 單元測試與Junit4測試框架:
單元測試:是指對軟件中的最小可測試單元進行檢查和驗證。在java應用程序中常常指的是一個方法(但并不總是如此)。
Junit4測試框架:框架是一個應用程序的半成品。框架提供了可在應用程序之間共享的課服用的公共結構。例如下面在Eclipse中運行Junit4生成的框架:
package com.huawei.osm.incident.utils;
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
public class MybatisUtilsTest
{? ?
? ? @BeforeClass
? ? public static void setUpBeforeClass() throws Exception
? ? {
? ? }? ?
? ? @AfterClass
? ? public static void tearDownAfterClass() throws Exception
? ? {
? ? }? ?
? ? @Before
? ? public void setUp() throws Exception
? ? {
? ? }? ?
? ? @After
? ? public void tearDown() throws Exception
? ? {
? ? }? ?
? ? @Test
? ? public void testGetSqlSession()
? ? {
? ? ? ? fail("Not yet implemented");
? ? }? ?
? ? @Test
? ? public void testCloseSqlSession()
? ? {
? ? ? ? fail("Not yet implemented");
? ? }? ?
}
2 了解Junit4新特性:
2.1 Junit4新特性
1. 必須引入的Test類:import org.junit.Test。舊的Junit3是需要繼承Test。以及新的類:org.junit.Assert.*。
2. 采用@Befor和@After加載或者清除資源。相當于許多方法類中的init()和destroy();
3. 屬于類范圍的 setUp()方法 和 tearDown() 方法,任何用 @BeforeClass 注釋的方法都將在該類中的測試方法運行之前運行一次,而任何用 @AfterClass 注釋的方法都將在該類中的所有測試都運行之后運行一次;
4. 提供@Test(expected=“可能拋出的異常”)對異常測試的注解
5. 提供@lgnore注解,意即被@Ignore標記的方法會在運行測試時跳過,或者說被改元素標記的方法在測試中會被忽略。當測試的方法還沒有實現,或者測試的方法已經過時,或者在某種條件下才能測試該方法(比如需要一個數據庫聯接,而在本地測試的時候,數據庫并沒有連接),那么使用該標簽來標示這個方法。同時,你可以為該標簽傳遞一個String的參數,來表明為什么會忽略這個測試方法。比如:@lgnore(“該方法還沒有實現”),在執行的時候,僅會報告該方法沒有實現,而不會運行測試方法。
6. 提供測試響應時間@Test(timeout=1000)(單位:毫秒),如果測試的運行時間超過指定的毫秒數,即認為測試失敗。
7. 增加兩個斷言方法:
(1)public static void assertEquals(Object[] expected, Object[] actual)
(2)public static void assertEquals(String message, Object[] expected, Object[] actual) 這兩用來比較數組:如果數組的長度和對應的元素相同,測這兩個數組相等,否則不等,也考慮了數組為空的情況。
2.2 Junit4中常見的幾個annotation釋義
@Test:測試方法,可以測試期望異常和超市時間
@Before:初始化方法,每次執行@Test之前都會運行一次。
@After:資源釋放,每次執行@Test之后都會運行一次。
@Ignore:忽略的測試方法
@BeforeClass、@AfterClass:注意區分@Before,它是針對所有的測試,在所在類中只會執行一次,只能定義為static void。同理可以接@AfterClass。
2.3 Junit4的單元測試用例執行順序為
@BeforeClass>>@Before>>@Test>>@After>>@AfterClass.
3 Junit4測試案例
3.1 Eclipse中使用Junit4
下面是我們在Eclipse中新建的一個用來做測試的一個計算器類Claculator
package com.huawei.demo;
public class Calculator
{
? ? private static int result;
? ? public void add(int n)
? ? {
? ? ? ? result = result + n;? ? ? ?
? ? }
? ? public void subs(int n)
? ? {?
? ? ? ? result = result - n;? ? ? ?
? ? }
? ? // 此方法尚未寫好
public void multiply(int n)
{? ?
// 此方法尚未寫好? ?
? ? }? ?
? ? public void divide(int n)
? ? {
? ? ? ? result = result / n;
? ? }
? ? public void square(int n)
? ? {
? ? ? ? result = n * n;
? ? }
? ? //Bug : 死循環
? ? public void squareRoot(int n)
? ? {
? ? ? ? for (; ;) ;?
? ? }
? ? // 將結果清零
? ? public void clear()
? ? {
? ? ? ? result = 0;
? ? }
? ? public int getResult()
? ? {
? ? ? ? return result;
}
}
第二步,在我們的項目中引入JUnit4的jar包(略)
第三步,右鍵點擊我們剛才新建的Claculator類,選擇“New”新建一個“JUnit Test Case”,如圖:
點擊“下一步”,進入勾選測試方法的頁面,根據情況選擇需要做測試的單元,如圖:
點擊“完成”。Eclipse會自動幫我們生成Junit4的測試框架,如下:
package com.huawei.demoTest;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
public class CalculatorTest
{? ?
? ? @Before
? ? public void setUp() throws Exception
? ? {
? ? }? ?
? ? @Test
? ? public void testAdd()
? ? {
? ? ? ? fail("Not yet implemented");
? ? }? ?
? ? @Test
? ? public void testSubs()
? ? {
? ? ? ? fail("Not yet implemented");
? ? }? ?
? ? @Test
? ? public void testMultiply()
? ? {
? ? ? ? fail("Not yet implemented");
? ? }? ?
? ? @Test
? ? public void testDivide()
? ? {
? ? ? ? fail("Not yet implemented");
? ? }? ?
? ? @Test
? ? public void testSquare()
? ? {
? ? ? ? fail("Not yet implemented");
? ? }? ?
? ? @Test
? ? public void testSquareRoot()
? ? {
? ? ? ? fail("Not yet implemented");
? ? }
}
第四步:編寫我們的測試內容。(在Calculator類中預留的bug和exception都得處理好。)修改后如下:
package com.huawei.demoTest;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import com.huawei.demo.Calculator;
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);
calculator.add(-5);
? ? ? ? assertEquals(0, calculator.getResult());
? ? }? ?
? ? @Test
? ? public void testSubs()
? ? {
? ? ? ? calculator.add(10);
? ? ? ? calculator.subs(2);
? ? ? ? assertEquals(8, calculator.getResult());
? ? }
? ? @Ignore("Multiply() Not yet implemented")
? ? @Test
? ? public void testMultiply()
? ? {
? ? }? ?
? ? @Test
? ? public void testDivide()
? ? {
? ? ? ? calculator.add(16);
? ? ? ? calculator.divide(-2);
calculator.divide(-2);
? ? ? ? assertEquals(4, calculator.getResult());
? ? }
? ? @Test(expected = ArithmeticException.class)
? ? public void divideByZero()
? ? {
? ? ? ? calculator.divide(0);
? ? }
? ? @Test
? ? public void testSquare()
{
calculator.square(-2);
? ? ? ? calculator.square(4);
? ? ? ? assertEquals(16, calculator.getResult());
? ? }? ?
? ? @Test(timeout = 1000)
? ? public void testSquareRoot()
? ? {
? ? ? ? calculator.squareRoot(4);
? ? ? ? assertEquals(2, calculator.getResult());
}
}
如下:運行改單元測試案例,可以查看測試結果:
3.2 Gradle中集成Junit4單元測試
第一步:按照gradle項目目錄結構新建一個“gradle-junit”的項目工程,該工程下包含標示gradle項目的build.gradle文件。并將我們剛才編寫的Calculator類和它的單元測試案例類CalculatorTest放在對應的目錄中。
第二步:編寫我們的build.gradle文件,如下
第三步:執行gradle命令。命令行模式下,進入我們剛才仙劍的“gradle-junit”工程所在的目錄,執行gradle test命令。等待gradle構建完畢,從窗口可以看到本次執行的結果,如下:
結果分析解讀:
1. 執行testSquareRoot(死循環)測試失敗,并報TestTimedOutException異常;
2. 總共測試了7個tests,1個失敗了(failed),1個跳過了(skipped);
3. 最后最大的亮點在,gradle為我們生成了本次測試的html頁面報告文件(…/build/reports/tests/test/index.html),可以非常直觀的顯示本次構建結果,并且還支持鏈接查看類和我們剛才編寫的單元測試案例。
4. 點擊紅色標記的包或者類可以進入相應的鏈接頁面,查看更詳細的結果。例如點擊下面的CalculatorTest鏈接可以查看測試失敗單元測試案例原因:
TestTimedOutException:test timed out after 1000 milliseconds
以及測試類的一個測試結果類的小結,如下圖:
(可以看見執行testSquareRoot測試任務花費1.010s,超過我們設定的閥值1s;
并且testMultiply測試任務變色。)
4 單元測試代碼覆蓋率
在做單元測試時,代碼覆蓋率常常被拿來作為衡量測試好壞的指標,甚至,有用代碼覆蓋率來考核測試任務完成情況,比如代碼覆蓋率必須達到80%或90%。正確理解代碼覆蓋率,對我們設計案例覆蓋代碼很有必要,因為僅僅使用代碼覆蓋率來衡量,有利也有弊。
4.1 語句覆蓋
語句覆蓋就是度量被測試代碼中每個可執行語句是否被執行到了,簡單的說就是只統計能夠執行的代碼被執行了多少行。語句覆蓋常常被人指責為“最弱的覆蓋”,它只管覆蓋代碼中執行語句,卻不考慮各種分支的組合等等。例如:
測試代碼如下:
int foo (int a,int b)
{
return a/b;
}
假如我們設計如下的測試案例:
TeseCase:a = 10,b = 5
雖然代碼覆蓋率到達了100%,并且所有的測試案例都通過了。然而遺憾的是,我們的語句覆蓋率到達了所謂的100%,但是卻沒有發現最賤的bug,比如當我們取b=0時,會拋出異常。再如下面的例子:
int foo (int a, int b)
{
int result = 0
if (a < 10) {
result += 1;
}
if (b < 10) {
result += 10;
}
return resule;
}
設計的測試案例如下:
TeseCase:a = 5, b = 5
該案例保證了測試案例的語句都執行了,包括分支中執行的語句,因此語句覆蓋率也達到了100%,但是從分支邏輯上明顯覆蓋不全。
4.2 判定覆蓋和條件覆蓋
判定覆蓋又稱分支覆蓋,它是度量程序中沒一個判定的分支是否都被測試到了。這句話需要進一步理解,很容易和條件覆蓋混淆。我們看例子:
int foo (int a, int b)
{
if (a < 10 || b < 10)
{
return 0; //分支一
}else {
return 1;//分支二
}
}
設計判定覆蓋案例時,我們只需考慮判定結果為true和false兩種情況,我們設計如下的案例就能達到判定覆蓋率為100%:
TeseCase1:a = 5, b = 任意數 覆蓋了分支一
TeseCase2:a = 15, b = 15 覆蓋了分支二
從設計條件的邏輯上出發,TeseCase2的設計與TeseCase1的在邏輯上完全的互斥互逆的,簡單理解為除了分支一的情況,就只剩下分支二了。
設計條件覆蓋案例時,我們需要考慮判定中的每個條件表達結果,為了覆蓋率達到100%,我們設計如下的案例:
TeseCase1:a = 5, b = 5 true, true
TeseCase2:a = 15,b = 15 false, false
由此可見,條件覆蓋不是講判定中的每一個田間表達式的結果進行排列組合,而只要每個條件表達式的結果為true和false測試到了就OK了。說白了,條件覆蓋就是不考慮邏輯分支僅覆蓋條件分支,執行條件分支語句的覆蓋。因此,我們得出這樣的推論:完全的條件覆蓋并不能覆蓋保證完全的判定覆蓋。
4.3 路徑覆蓋
對于路徑覆蓋的理解,我們設計如下的案例:
int foo (int a, int b)
{
int result = 0
if (a < 10) {
result += 1;
}
if (b < 10) {
result += 10;
}
return resule;
}
測試案例:
TsseCase1:a = 5, b = 5
TeseCase2:a = 5, b = 15
TeseCase3:a = 15, b = 5
TeseCase4:a = 15, b = 15
路徑覆蓋又稱斷言覆蓋。它度量了是否函數的每一個分支都被執行了。這句話也非常好理解,就是所有的可能分支都執行一遍,有多個分支嵌套時,需要對多個分支進行排列組合,可想而知,測試的路徑隨著分支的數量呈2?指數級別的增加。
5 Jacoco單元測試覆蓋率
Jacoco專門用于統計單元測試覆蓋率的,在gradle中和‘java’插件一樣使用時直接引用,并添加版本即可:
apply plugin: 'jacoco'
jacoco{? toolVersion = "0.7.1.201405082137"? }
運行jacoco也非常簡單,因為他依賴于單元測試的完成,所以執行完test任務后,可以緊接著執行gradle jacoco命令,執行成功,也會生成一個類似單元測試的html結果報告,非常直觀的可以看到統計數據的結果。
5.1 Jacoco案例分析(一):語句覆蓋與判定覆蓋
源碼如下:
package com.huawei.demo;
public class CoverJudge
{
? ? public int foot(int a, int b)
? ? {
? ? ? ? int result = 0;
? ? ? ? if (a < 10)
? ? ? ? {
? ? ? ? ? ? result += 1;
? ? ? ? }
? ? ? ? if (b < 10)
? ? ? ? {
? ? ? ? ? ? result += 10;
? ? ? ? }
? ? ? ? return result;
? ? }? ?
}
下面是設計的單元測試案例:
package com.huawei.demoTest;
import static org.junit.Assert.*;
import org.junit.Test;
import com.huawei.demo.CoverJudge;
public class CoverJudgeTest
{
? ? private static CoverJudge coverJudge = new CoverJudge();
//簡單的語句覆蓋
@Test
? ? public void testFoot()
? ? {
? ? ? ? int a = 5;
? ? ? ? int b = 5;
? ? ? ? assertEquals(11, coverJudge.foot(a, b));
? ? }
? ? /*@Test
? ? public void testFoot()
? ? {
? ? ? ? int a = 5;
? ? ? ? int b = 15;
? ? ? ? assertEquals(1, coverJudge.foot(a, b));
? ? }
@Test
? ? public void testFoot1()
? ? {
? ? ? ? int a = 15;
? ? ? ? int b = 5;
? ? ? ? assertEquals(10, coverJudge.foot(a, b));
? ? }*/? ?
}
運行gradle test jacoco,構建完成后可以查看覆蓋率報告,如下:
顯示分支覆蓋率(Missed Branches)僅為50%。
5.2 Jacoco案例分析(二):多分支覆蓋
源碼如下:
package com.huawei.demo;
public class MathUtil
{
? ? public int max(int a, int b, int c)
? ? {
? ? ? ? if (a > b)
? ? ? ? {
? ? ? ? ? ? if (a > c)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? return a;
? ? ? ? ? ? }
? ? ? ? ? ? else
? ? ? ? ? ? {
? ? ? ? ? ? ? ? return c;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? else
? ? ? ? {
? ? ? ? ? ? if (b > c)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? return b;
? ? ? ? ? ? }
? ? ? ? ? ? else
? ? ? ? ? ? {
? ? ? ? ? ? ? ? return c;
? ? ? ? ? ? }
? ? ? ? }
? ? }
}
下面是設計的單元測試案例:
TeseCase1:
package com.huawei.demoTest;
import static org.junit.Assert.*;
import org.junit.Test;
import com.huawei.demo.MathUtil;
public class MathUtilTest
{
? ? private static MathUtil mathUtil = new MathUtil();
? ? @Test
? ? public void testMax1_2_3()
? ? {
? ? ? ? assertEquals(3, mathUtil.max(1, 2, 3));
? ? }? ?
}
執行后結果如下,還可以進入鏈接查看詳情
TestCase2:
package com.huawei.demoTest;
import static org.junit.Assert.*;
import org.junit.Test;
import com.huawei.demo.MathUtil;
public class MathUtilTest2
{
? ? private static MathUtil mathUtil = new MathUtil();
? ? @Test
? ? public void testMax1_2_3()
? ? {
? ? ? ? assertEquals(3, mathUtil.max(1, 2, 3));
? ? }
@Test
public void test_max_1_3_2() {
assertEquals(3, mathUtil.max(1, 3, 2));
}
@Test
public void test_max_3_2_1() {
assertEquals(3, mathUtil.max(3, 2, 1));
}
@Test
public void test_max_0_0_0(){
assertEquals(0, mathUtil.max(0, 0, 0));
}
@Test
public void test_max_0_1_0(){
assertEquals(1, mathUtil.max(0, 1, 0));
}? ?
}
執行后結果如下:
點擊鏈接還可以查詢沒有覆蓋到的分支:
紅色表示沒有覆蓋到的分支;分析沒有覆蓋到a > b且a < c的情況,所以可以設計如下Tesecase:a = 2, b = 1, c = 3的情況。
所以增加如下單元測試案例:
@Test
public void test_max_2_1_3() {
assertEquals(3, mathUtil.max(2, 1, 3));
}
再次執行可以顯示100%的分支代碼覆蓋率。