單元測試是編寫測試代碼,用來檢測特定的、明確的、細顆粒的功能。單元測試並不一定保證程序功能是正確的,更不保證整體業務是准備的。
單元測試不僅僅用來保證當前代碼的正確性,更重要的是用來保證代碼修復、改進或重構之后的正確性。
一般來說,單元測試任務包括
JUNIT
JUnit是Java單元測試框架,已經在Eclipse中默認安裝。目前主流的有JUnit3和JUnit4。JUnit3中,測試用例需要繼承TestCase
類。JUnit4中,測試用例無需繼承TestCase
類,只需要使用@Test
等注解。
Junit3
先看一個Junit3的樣例
// 測試java.lang.Math // 必須繼承TestCase public class Junit3TestCase extends TestCase { public Junit3TestCase() { super(); } // 傳入測試用例名稱 public Junit3TestCase(String name) { super(name); } // 在每個Test運行之前運行 @Override protected void setUp() throws Exception { System.out.println("Set up"); } // 測試方法。 // 方法名稱必須以test開頭,沒有參數,無返回值,是公開的,可以拋出異常 // 也即類似public void testXXX() throws Exception {} public void testMathPow() { System.out.println("Test Math.pow"); Assert.assertEquals(4.0, Math.pow(2.0, 2.0)); } public void testMathMin() { System.out.println("Test Math.min"); Assert.assertEquals(2.0, Math.min(2.0, 4.0)); } // 在每個Test運行之后運行 @Override protected void tearDown() throws Exception { System.out.println("Tear down"); } }
如果采用默認的TestSuite,則測試方法必須是public void testXXX() [throws Exception] {}
的形式,並且不能存在依賴關系,因為測試方法的調用順序是不可預知的。
上例執行后,控制台會輸出
Set up
Test Math.pow
Tear down
Set up
Test Math.min
Tear down
從中,可以猜測到,對於每個測試方法,調用的形式是:
testCase.setUp();
testCase.testXXX();
testCase.tearDown();
運行測試方法
在Eclipse中,可以直接在類名或測試方法上右擊,在彈出的右擊菜單中選擇Run As -> JUnit Test。
在Mvn中,可以直接通過mvn test
命令運行測試用例。
也可以通過Java方式調用,創建一個TestCase
實例,然后重載runTest()
方法,在其方法內調用測試方法(可以多個)。
TestCase test = new Junit3TestCase("mathPow") { // 重載 protected void runTest() throws Throwable { testMathPow(); }; }; test.run();
更加便捷地,可以在創建TestCase
實例時直接傳入測試方法名稱,JUnit會自動調用此測試方法,如
TestCase test = new Junit3TestCase("testMathPow"); test.run();
Junit TestSuite
TestSuite是測試用例套件,能夠運行過個測試方法。如果不指定TestSuite,會創建一個默認的TestSuite。默認TestSuite會掃描當前內中的所有測試方法,然后運行。
如果不想采用默認的TestSuite,則可以自定義TestSuite。在TestCase中,可以通過靜態方法suite()
返回自定義的suite。
import junit.framework.Assert; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; public class Junit3TestCase extends TestCase { //... public static Test suite() { System.out.println("create suite"); TestSuite suite = new TestSuite(); suite.addTest(new Junit3TestCase("testMathPow")); return suite; } }
允許上述方法,控制台輸出
Set up
Test Math.pow
Tear down
並且只運行了testMathPow
測試方法,而沒有運行testMathMin
測試方法。通過顯式指定測試方法,可以控制測試執行的順序。
也可以通過Java的方式創建TestSuite,然后調用TestCase,如
// 先創建TestSuite,再添加測試方法 TestSuite testSuite = new TestSuite(); testSuite.addTest(new Junit3TestCase("testMathPow")); // 或者 傳入Class,TestSuite會掃描其中的測試方法。 TestSuite testSuite = new TestSuite(Junit3TestCase.class,Junit3TestCase2.class,Junit3TestCase3.class); // 運行testSuite TestResult testResult = new TestResult(); testSuite.run(testResult);
testResult中保存了很多測試數據,包括運行測試方法數目(runCount
)等。
JUnit4
與JUnit3不同,JUnit4通過注解的方式來識別測試方法。目前支持的主要注解有:
下面舉一個樣例:
import org.junit.After; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; public class Junit4TestCase { @BeforeClass public static void setUpBeforeClass() { System.out.println("Set up before class"); } @Before public void setUp() throws Exception { System.out.println("Set up"); } @Test public void testMathPow() { System.out.println("Test Math.pow"); Assert.assertEquals(4.0, Math.pow(2.0, 2.0), 0.0); } @Test public void testMathMin() { System.out.println("Test Math.min"); Assert.assertEquals(2.0, Math.min(2.0, 4.0), 0.0); } // 期望此方法拋出NullPointerException異常 @Test(expected = NullPointerException.class) public void testException() { System.out.println("Test exception"); Object obj = null; obj.toString(); } // 忽略此測試方法 @Ignore @Test public void testMathMax() { Assert.fail("沒有實現"); } // 使用“假設”來忽略測試方法 @Test public void testAssume(){ System.out.println("Test assume"); // 當假設失敗時,則會停止運行,但這並不會意味測試方法失敗。 Assume.assumeTrue(false); Assert.fail("沒有實現"); } @After public void tearDown() throws Exception { System.out.println("Tear down"); } @AfterClass public static void tearDownAfterClass() { System.out.println("Tear down After class"); } }
如果細心的話,會發現Junit3的package是junit.framework
,而Junit4是org.junit
。
執行此用例后,控制台會輸出
Set up
Test Math.pow
Tear down
Set up
Test Math.min
Tear down
Set up
Test exception
Tear down
Set up
Test assume
Tear down
Tear down After class
可以看到,執行次序是@BeforeClass
-> @Before
-> @Test
-> @After
-> @Before
-> @Test
-> @After
-> @AfterClass
。@Ignore
會被忽略。
運行測試方法
與Junit3類似,可以在Eclipse中運行,也可以通過mvn test
命令運行。
Assert
Junit3和Junit4都提供了一個Assert類(雖然package不同,但是大致差不多)。Assert類中定義了很多靜態方法來進行斷言。列表如下:
Mock/Stub
Mock和Stub是兩種測試代碼功能的方法。Mock測重於對功能的模擬。Stub測重於對功能的測試重現。比如對於List接口,Mock會直接對List進行模擬,而Stub會新建一個實現了List的TestList,在其中編寫測試的代碼。
強烈建議優先選擇Mock方式,因為Mock方式下,模擬代碼與測試代碼放在一起,易讀性好,而且擴展性、靈活性都比Stub好。
比較流行的Mock有:
其中EasyMock和Mockito對於Java接口使用接口代理的方式來模擬,對於Java類使用繼承的方式來模擬(也即會創建一個新的Class類)。Mockito支持spy方式,可以對實例進行模擬。但它們都不能對靜態方法和final類進行模擬,powermock通過修改字節碼來支持了此功能。
EasyMock
IBM上有幾篇介紹EasyMock使用方法和原理的文章:EasyMock 使用方法與原理剖析,使用 EasyMock 更輕松地進行測試。
EasyMock把測試過程分為三步:錄制、運行測試代碼、驗證期望。
錄制過程大概就是:期望method(params)執行times次(默認一次),返回result(可選),拋出exception異常(可選)。
驗證期望過程將會檢查方法的調用次數。
一個簡單的樣例是:
@Test public void testListInEasyMock() { List list = EasyMock.createMock(List.class); // 錄制過程 // 期望方法list.set(0,1)執行2次,返回null,不拋出異常 expect1: EasyMock.expect(list.set(0, 1)).andReturn(null).times(2); // 期望方法list.set(0,1)執行1次,返回null,不拋出異常 expect2: EasyMock.expect(list.set(0, 1)).andReturn(1); // 執行測試代碼 EasyMock.replay(list); // 執行list.set(0,1),匹配expect1期望,會返回null Assert.assertNull(list.set(0, 1)); // 執行list.set(0,1),匹配expect1(因為expect1期望執行此方法2次),會返回null Assert.assertNull(list.set(0, 1)); // 執行list.set(0,1),匹配expect2,會返回1 Assert.assertEquals(1, list.set(0, 1)); // 驗證期望 EasyMock.verify(list); }
EasyMock還支持嚴格的檢查,要求執行的方法次序與期望的完全一致。
Mockito
Mockito是Google Code上的一個開源項目,Api相對於EasyMock更好友好。與EasyMock不同的是,Mockito沒有錄制過程,只需要在“運行測試代碼”之前對接口進行Stub,也即設置方法的返回值或拋出的異常,然后直接運行測試代碼,運行期間調用Mock的方法,會返回預先設置的返回值或拋出異常,最后再對測試代碼進行驗證。可以查看此文章了解兩者的不同。
官方提供了很多樣例,基本上包括了所有功能,可以去看看。
這里從官方樣例中摘錄幾個典型的:
- 驗證調用行為
import static org.mockito.Mockito.*; //創建Mock List mockedList = mock(List.class); //使用Mock對象 mockedList.add("one"); mockedList.clear(); //驗證行為 verify(mockedList).add("one"); verify(mockedList).clear();
- 對Mock對象進行Stub
//也可以Mock具體的類,而不僅僅是接口 LinkedList mockedList = mock(LinkedList.class); //Stub when(mockedList.get(0)).thenReturn("first"); // 設置返回值 when(mockedList.get(1)).thenThrow(new RuntimeException()); // 拋出異常 //第一個會打印 "first" System.out.println(mockedList.get(0)); //接下來會拋出runtime異常 System.out.println(mockedList.get(1)); //接下來會打印"null",這是因為沒有stub get(999) System.out.println(mockedList.get(999)); // 可以選擇性地驗證行為,比如只關心是否調用過get(0),而不關心是否調用過get(1) 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來分析。
public void createReport() throws Exception { // 讀取監控結果 final FileInputStream fis = new FileInputStream(new File("jacoco.exec")); final ExecutionDataReader executionDataReader = new ExecutionDataReader(fis); // 執行數據信息 ExecutionDataStore executionDataStore = new ExecutionDataStore(); // 會話信息 SessionInfoStore sessionInfoStore = new SessionInfoStore(); executionDataReader.setExecutionDataVisitor(executionDataStore); executionDataReader.setSessionInfoVisitor(sessionInfoStore); while (executionDataReader.read()) { } fis.close(); // 分析結構 final CoverageBuilder coverageBuilder = new CoverageBuilder(); final Analyzer analyzer = new Analyzer(executionDataStore, coverageBuilder); // 傳入監控時的Class文件目錄,注意必須與監控時的一樣 File classesDirectory = new File("classes"); analyzer.analyzeAll(classesDirectory); IBundleCoverage bundleCoverage = coverageBuilder.getBundle("Title"); // 輸出報告 File reportDirectory = new File("report"); // 報告所在的目錄 final HTMLFormatter htmlFormatter = new HTMLFormatter(); // HTML格式 final IReportVisitor visitor = htmlFormatter.createVisitor(new FileMultiReportOutput(reportDirectory)); // 必須先調用visitInfo visitor.visitInfo(sessionInfoStore.getInfos(), executionDataStore.getContents()); File sourceDirectory = new File("src"); // 源代碼目錄 // 遍歷所有的源代碼 // 如果不執行此過程,則在報告中只能看到方法名,但是無法查看具體的覆蓋(因為沒有源代碼頁面) visitor.visitBundle(bundleCoverage, new DirectorySourceFileLocator(sourceDirectory, "utf-8", 4)); // 執行完畢 visitor.visitEnd(); }
轉載於:https://blog.csdn.net/iteye_14419/article/details/82473085