天天看點

Java單元測試(Junit+Mock+代碼覆寫率)

​​​​

單元測試是編寫測試代碼,用來檢測特定的、明确的、細顆粒的功能。單元測試并不一定保證程式功能是正确的,更不保證整體業務是準備的。

單元測試不僅僅用來保證目前代碼的正确性,更重要的是用來保證代碼修複、改進或重構之後的正确性。

一般來說,單元測試任務包括

  1. 接口功能測試:用來保證接口功能的正确性。
  2. 局部資料結構測試(不常用):用來保證接口中的資料結構是正确的
  1. 比如變量有無初始值
  2. 變量是否溢出
  1. 邊界條件測試
  1. 變量沒有指派(即為NULL)
  2. 變量是數值(或字元)
  1. 主要邊界:最小值,最大值,無窮大(對于DOUBLE等)
  2. 溢出邊界(期望異常或拒絕服務):最小值-1,最大值+1
  3. 臨近邊界:最小值+1,最大值-1
  1. 變量是字元串
  1. 引用“字元變量”的邊界
  2. 空字元串
  3. 對字元串長度應用“數值變量”的邊界
  1. 變量是集合
  1. 空集合
  2. 對集合的大小應用“數值變量”的邊界
  3. 調整次序:升序、降序
  1. 變量有規律
  1. 比如對于Math.sqrt,給出n^2-1,和n^2+1的邊界
  1. 所有獨立執行通路測試:保證每一條代碼,每個分支都經過測試
  1. 代碼覆寫率
  1. 語句覆寫:保證每一個語句都執行到了
  2. 判定覆寫(分支覆寫):保證每一個分支都執行到
  3. 條件覆寫:保證每一個條件都覆寫到true和false(即if、while中的條件語句)
  4. 路徑覆寫:保證每一個路徑都覆寫到
  1. 相關軟體
  1. Cobertura:語句覆寫
  2. Emma: Eclipse插件Eclemma
  1. 各條錯誤處理通路測試:保證每一個異常都經過測試

JUNIT

JUnit是Java單元測試架構,已經在Eclipse中預設安裝。目前主流的有JUnit3和JUnit4。JUnit3中,測試用例需要繼承​

​TestCase​

​類。JUnit4中,測試用例無需繼承​

​TestCase​

​類,隻需要使用​

​@Test​

​等注解。

Junit3

先看一個Junit3的樣例

Java代碼  

Java單元測試(Junit+Mock+代碼覆寫率)
  1. // 測試java.lang.Math  
  2. // 必須繼承TestCase  
  3. public class Junit3TestCase extends TestCase {  
  4.     public Junit3TestCase() {  
  5.         super();  
  6.     }  
  7.         // 傳入測試用例名稱  
  8.     public Junit3TestCase(String name) {  
  9.         super(name);  
  10.         // 在每個Test運作之前運作  
  11.     @Override  
  12.     protected void setUp() throws Exception {  
  13.         System.out.println("Set up");  
  14.         // 測試方法。  
  15.         // 方法名稱必須以test開頭,沒有參數,無傳回值,是公開的,可以抛出異常  
  16.         // 也即類似public void testXXX() throws Exception {}  
  17.     public void testMathPow() {  
  18.         System.out.println("Test Math.pow");  
  19.         Assert.assertEquals(4.0, Math.pow(2.0, 2.0));  
  20.     public void testMathMin() {  
  21.         System.out.println("Test Math.min");  
  22.         Assert.assertEquals(2.0, Math.min(2.0, 4.0));  
  23.         // 在每個Test運作之後運作  
  24.     protected void tearDown() throws Exception {  
  25.         System.out.println("Tear down");  
  26. }  

如果采用預設的TestSuite,則測試方法必須是​

​public void testXXX() [throws Exception] {}​

​的形式,并且不能存在依賴關系,因為測試方法的調用順序是不可預知的。

上例執行後,控制台會輸出

Text代碼  

Java單元測試(Junit+Mock+代碼覆寫率)
  1. Set up  
  2. Test Math.pow  
  3. Tear down  
  4. Test Math.min  

從中,可以猜測到,對于每個測試方法,調用的形式是:

Java單元測試(Junit+Mock+代碼覆寫率)
  1. testCase.setUp();  
  2. testCase.testXXX();  
  3. testCase.tearDown();     
運作測試方法

在Eclipse中,可以直接在類名或測試方法上右擊,在彈出的右擊菜單中選擇Run As -> JUnit Test。

在Mvn中,可以直接通過​

​mvn test​

​指令運作測試用例。

也可以通過Java方式調用,建立一個​

​TestCase​

​執行個體,然後重載​

​runTest()​

​方法,在其方法内調用測試方法(可以多個)。

Java單元測試(Junit+Mock+代碼覆寫率)
  1. TestCase test = new Junit3TestCase("mathPow") {  
  2.         // 重載  
  3.     protected void runTest() throws Throwable {  
  4.         testMathPow();  
  5.     };  
  6. };  
  7. test.run();  

更加便捷地,可以在建立​

​TestCase​

​執行個體時直接傳入測試方法名稱,JUnit會自動調用此測試方法,如

Java單元測試(Junit+Mock+代碼覆寫率)
  1. TestCase test = new Junit3TestCase("testMathPow");  
Junit TestSuite

TestSuite是測試用例套件,能夠運作過個測試方法。如果不指定TestSuite,會建立一個預設的TestSuite。預設TestSuite會掃描目前内中的所有測試方法,然後運作。

如果不想采用預設的TestSuite,則可以自定義TestSuite。在TestCase中,可以通過靜态方法​

​suite()​

​傳回自定義的suite。

Java單元測試(Junit+Mock+代碼覆寫率)
  1. import junit.framework.Assert;  
  2. import junit.framework.Test;  
  3. import junit.framework.TestCase;  
  4. import junit.framework.TestSuite;  
  5.         //...  
  6.     public static Test suite() {  
  7.         System.out.println("create suite");  
  8.         TestSuite suite = new TestSuite();  
  9.         suite.addTest(new Junit3TestCase("testMathPow"));  
  10.         return suite;  

允許上述方法,控制台輸出

寫道

create suite

Set up

Test Math.pow

Tear down

并且隻運作了​

​testMathPow​

​測試方法,而沒有運作​

​testMathMin​

​測試方法。通過顯式指定測試方法,可以控制測試執行的順序。

也可以通過Java的方式建立TestSuite,然後調用TestCase,如

Java單元測試(Junit+Mock+代碼覆寫率)
  1. // 先建立TestSuite,再添加測試方法  
  2. TestSuite testSuite = new TestSuite();  
  3. testSuite.addTest(new Junit3TestCase("testMathPow"));  
  4. // 或者 傳入Class,TestSuite會掃描其中的測試方法。  
  5. TestSuite testSuite = new TestSuite(Junit3TestCase.class,Junit3TestCase2.class,Junit3TestCase3.class);  
  6. // 運作testSuite  
  7. TestResult testResult = new TestResult();  
  8. testSuite.run(testResult);  

testResult中儲存了很多測試資料,包括運作測試方法數目(​

​runCount​

​)等。

JUnit4

與JUnit3不同,JUnit4通過注解的方式來識别測試方法。目前支援的主要注解有:

  • ​@BeforeClass​

    ​ 全局隻會執行一次,而且是第一個運作
  • ​@Before​

    ​ 在測試方法運作之前運作
  • ​@Test​

    ​ 測試方法
  • ​@After​

    ​ 在測試方法運作之後允許
  • ​@AfterClass​

    ​ 全局隻會執行一次,而且是最後一個運作
  • ​@Ignore​

    ​ 忽略此方法

下面舉一個樣例:

Java單元測試(Junit+Mock+代碼覆寫率)
  1. import org.junit.After;  
  2. import org.junit.AfterClass;  
  3. import org.junit.Assert;  
  4. import org.junit.Before;  
  5. import org.junit.BeforeClass;  
  6. import org.junit.Ignore;  
  7. import org.junit.Test;  
  8. public class Junit4TestCase {  
  9.     @BeforeClass  
  10.     public static void setUpBeforeClass() {  
  11.         System.out.println("Set up before class");  
  12.     @Before  
  13.     public void setUp() throws Exception {  
  14.     @Test  
  15.         Assert.assertEquals(4.0, Math.pow(2.0, 2.0), 0.0);  
  16.         Assert.assertEquals(2.0, Math.min(2.0, 4.0), 0.0);  
  17.         // 期望此方法抛出NullPointerException異常  
  18.     @Test(expected = NullPointerException.class)  
  19.     public void testException() {  
  20.         System.out.println("Test exception");  
  21.         Object obj = null;  
  22.         obj.toString();  
  23.         // 忽略此測試方法  
  24.     @Ignore  
  25.     public void testMathMax() {  
  26.           Assert.fail("沒有實作");  
  27.         // 使用“假設”來忽略測試方法  
  28.     public void testAssume(){  
  29.         System.out.println("Test assume");  
  30.                 // 當假設失敗時,則會停止運作,但這并不會意味測試方法失敗。  
  31.         Assume.assumeTrue(false);  
  32.         Assert.fail("沒有實作");  
  33.     @After  
  34.     public void tearDown() throws Exception {  
  35.     @AfterClass  
  36.     public static void tearDownAfterClass() {  
  37.         System.out.println("Tear down After class");  

如果細心的話,會發現Junit3的package是​

​junit.framework​

​,而Junit4是​

​org.junit​

​。

執行此用例後,控制台會輸出

Set up before class

Test Math.min

Test exception

Test assume

Tear down After class

可以看到,執行次序是​

​@BeforeClass​

​ -> ​

​@Before​

​@Test​

​@After​

​@Before​

​@Test​

​@After​

​@AfterClass​

​。​

​@Ignore​

​會被忽略。

與Junit3類似,可以在Eclipse中運作,也可以通過​

​mvn test​

​指令運作。

Assert

Junit3和Junit4都提供了一個Assert類(雖然package不同,但是大緻差不多)。Assert類中定義了很多靜态方法來進行斷言。清單如下:

  • assertTrue(String message, boolean condition) 要求condition == true
  • assertFalse(String message, boolean condition) 要求condition == false
  • fail(String message) 必然失敗,同樣要求代碼不可達
  • assertEquals(String message, XXX expected,XXX actual) 要求expected.equals(actual)
  • assertArrayEquals(String message, XXX[] expecteds,XXX [] actuals) 要求expected.equalsArray(actual)
  • assertNotNull(String message, Object object) 要求object!=null
  • assertNull(String message, Object object) 要求object==null
  • assertSame(String message, Object expected, Object actual) 要求expected == actual
  • assertNotSame(String message, Object unexpected,Object actual) 要求expected != actual
  • assertThat(String reason, T actual, Matcher matcher) 要求matcher.matches(actual) == true

Mock/Stub

Mock和Stub是兩種測試代碼功能的方法。Mock測重于對功能的模拟。Stub測重于對功能的測試重制。比如對于List接口,Mock會直接對List進行模拟,而Stub會建立一個實作了List的TestList,在其中編寫測試的代碼。

強烈建議優先選擇Mock方式,因為Mock方式下,模拟代碼與測試代碼放在一起,易讀性好,而且擴充性、靈活性都比Stub好。

比較流行的Mock有:

  • ​​JMock​​
  • ​​EasyMock​​
  • ​​Mockito​​
  • ​​powermock​​

其中EasyMock和Mockito對于Java接口使用接口代理的方式來模拟,對于Java類使用繼承的方式來模拟(也即會建立一個新的Class類)。Mockito支援spy方式,可以對執行個體進行模拟。但它們都不能對靜态方法和final類進行模拟,powermock通過修改位元組碼來支援了此功能。

EasyMock

IBM上有幾篇介紹EasyMock使用方法和原理的文章:​​EasyMock 使用方法與原理剖析​​,​​使用 EasyMock 更輕松地進行測試​​。

EasyMock把測試過程分為三步:錄制、運作測試代碼、驗證期望。

錄制過程大概就是:期望method(params)執行times次(預設一次),傳回result(可選),抛出exception異常(可選)。

驗證期望過程将會檢查方法的調用次數。

一個簡單的樣例是:

Java單元測試(Junit+Mock+代碼覆寫率)
  1. @Test  
  2. public void testListInEasyMock() {  
  3.     List list = EasyMock.createMock(List.class);  
  4.     // 錄制過程  
  5.     // 期望方法list.set(0,1)執行2次,傳回null,不抛出異常  
  6.     expect1: EasyMock.expect(list.set(0, 1)).andReturn(null).times(2);  
  7.     // 期望方法list.set(0,1)執行1次,傳回null,不抛出異常  
  8.     expect2: EasyMock.expect(list.set(0, 1)).andReturn(1);  
  9.     // 執行測試代碼  
  10.     EasyMock.replay(list);  
  11.         // 執行list.set(0,1),比對expect1期望,會傳回null  
  12.     Assert.assertNull(list.set(0, 1));  
  13.         // 執行list.set(0,1),比對expect1(因為expect1期望執行此方法2次),會傳回null  
  14.         // 執行list.set(0,1),比對expect2,會傳回1  
  15.     Assert.assertEquals(1, list.set(0, 1));  
  16.     // 驗證期望  
  17.     EasyMock.verify(list);  

EasyMock還支援嚴格的檢查,要求執行的方法次序與期望的完全一緻。

Mockito

Mockito是Google Code上的一個開源項目,Api相對于EasyMock更好友好。與EasyMock不同的是,Mockito沒有錄制過程,隻需要在“運作測試代碼”之前對接口進行Stub,也即設定方法的傳回值或抛出的異常,然後直接運作測試代碼,運作期間調用Mock的方法,會傳回預先設定的傳回值或抛出異常,最後再對測試代碼進行驗證。可以檢視​​此文章​​了解兩者的不同。

官方提供了很多樣例,基本上包括了所有功能,​​可以去看看​​。

這裡從官方樣例中摘錄幾個典型的:

  • 驗證調用行為
  1. import static org.mockito.Mockito.*;  
  2. //建立Mock  
  3. List mockedList = mock(List.class);  
  4. //使用Mock對象  
  5. mockedList.add("one");  
  6. mockedList.clear();  
  7. //驗證行為  
  8. verify(mockedList).add("one");  
  9. verify(mockedList).clear();  
  • 對Mock對象進行Stub
  1. //也可以Mock具體的類,而不僅僅是接口  
  2. LinkedList mockedList = mock(LinkedList.class);  
  3. //Stub  
  4. when(mockedList.get(0)).thenReturn("first"); // 設定傳回值  
  5. when(mockedList.get(1)).thenThrow(new RuntimeException()); // 抛出異常  
  6. //第一個會列印 "first"  
  7. System.out.println(mockedList.get(0));  
  8. //接下來會抛出runtime異常  
  9. System.out.println(mockedList.get(1));  
  10. //接下來會列印"null",這是因為沒有stub get(999)  
  11. System.out.println(mockedList.get(999));  
  12. // 可以選擇性地驗證行為,比如隻關心是否調用過get(0),而不關心是否調用過get(1)  
  13. verify(mockedList).get(0);  

比較流行的工具是Emma和​​Jacoco​​,Ecliplse插件有eclemma。eclemma2.0之前采用的是Emma,之後采用的是Jacoco。這裡主要介紹一下Jacoco。Eclmama由于是Eclipse插件,是以非常易用,就不多做介紹了。

Jacoco

Jacoco可以嵌入到Ant、Maven中,也可以使用Java Agent技術監控任意Java程式,也可以使用Java Api來定制功能。

Jacoco會監控JVM中的調用,生成監控結果(預設儲存在jacoco.exec檔案中),然後分析此結果,配合源代碼生成覆寫率報告。需要注意的是:監控和分析這兩步,必須使用相同的Class檔案,否則由于Class不同,而無法定位到具體的方法,導緻覆寫率均為0%。

Java Agent嵌入

首先,需要下載下傳jacocoagent.jar檔案,然後在Java程式啟動參數後面加上 ​

​-javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]​

​,具體的options可以在​​此頁面​​找到。預設會在JVM關閉時(注意不能是​

​kill -9​

​),輸出監控結果到jacoco.exec檔案中,也可以通過socket來實時地輸出監控報告(可以在Example代碼中找到簡單實作)。

Java Report

可以使用Ant、Mvn或Eclipse來分析jacoco.exec檔案,也可以通過API來分析。

Java單元測試(Junit+Mock+代碼覆寫率)
  1. public void createReport() throws Exception {  
  2.             // 讀取監控結果  
  3.     final FileInputStream fis = new FileInputStream(new File("jacoco.exec"));  
  4.     final ExecutionDataReader executionDataReader = new ExecutionDataReader(fis);  
  5.             // 執行資料資訊  
  6.     ExecutionDataStore executionDataStore = new ExecutionDataStore();  
  7.             // 會話資訊  
  8.     SessionInfoStore sessionInfoStore = new SessionInfoStore();  
  9.     executionDataReader.setExecutionDataVisitor(executionDataStore);  
  10.     executionDataReader.setSessionInfoVisitor(sessionInfoStore);  
  11.     while (executionDataReader.read()) {  
  12.     fis.close();  
  13.             // 分析結構  
  14.             final CoverageBuilder coverageBuilder = new CoverageBuilder();  
  15.     final Analyzer analyzer = new Analyzer(executionDataStore, coverageBuilder);  
  16.             // 傳入監控時的Class檔案目錄,注意必須與監控時的一樣  
  17.     File classesDirectory = new File("classes");  
  18.     analyzer.analyzeAll(classesDirectory);  
  19.     IBundleCoverage bundleCoverage = coverageBuilder.getBundle("Title");  
  20.             // 輸出報告  
  21.         File reportDirectory = new File("report"); // 報告所在的目錄  
  22.     final HTMLFormatter htmlFormatter = new HTMLFormatter();  // HTML格式  
  23.     final IReportVisitor visitor = htmlFormatter.createVisitor(new FileMultiReportOutput(reportDirectory));  
  24.             // 必須先調用visitInfo  
  25.     visitor.visitInfo(sessionInfoStore.getInfos(), executionDataStore.getContents());  
  26.     File sourceDirectory = new File("src"); // 源代碼目錄  
  27.             // 周遊所有的源代碼  
  28.             // 如果不執行此過程,則在報告中隻能看到方法名,但是無法檢視具體的覆寫(因為沒有源代碼頁面)  
  29.     visitor.visitBundle(bundleCoverage, new DirectorySourceFileLocator(sourceDirectory, "utf-8", 4));  
  30.             // 執行完畢  
  31.     visitor.visitEnd();  

出處:​​javascript:void(0)​​

原文見​​此處​​

​TestCase​

​TestCase​

​@Test​

Java單元測試(Junit+Mock+代碼覆寫率)

​public void testXXX() [throws Exception] {}​

Java單元測試(Junit+Mock+代碼覆寫率)
Java單元測試(Junit+Mock+代碼覆寫率)

​mvn test​

​TestCase​

​runTest()​

Java單元測試(Junit+Mock+代碼覆寫率)

​TestCase​

Java單元測試(Junit+Mock+代碼覆寫率)

​suite()​

Java單元測試(Junit+Mock+代碼覆寫率)

​testMathPow​

​testMathMin​

Java單元測試(Junit+Mock+代碼覆寫率)

​runCount​

  • ​@BeforeClass​

  • ​@Before​

  • ​@Test​

  • ​@After​

  • ​@AfterClass​

  • ​@Ignore​

Java單元測試(Junit+Mock+代碼覆寫率)

​junit.framework​

​org.junit​

​@BeforeClass​

​@Before​

​@Test​

​@After​

​@Before​

​@Test​

​@After​

​@AfterClass​

​@Ignore​

​mvn test​

Java單元測試(Junit+Mock+代碼覆寫率)

​-javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]​

​kill -9​

Java單元測試(Junit+Mock+代碼覆寫率)