單元測試是編寫測試代碼,用來檢測特定的、明確的、細顆粒的功能。單元測試並不必定保證程序功能是正確的,更不保證總體業務是準備的。 html
單元測試不只僅用來保證當前代碼的正確性,更重要的是用來保證代碼修復、改進或重構以後的正確性。 java
通常來講,單元測試任務包括 session
- 接口功能測試:用來保證接口功能的正確性。
- 局部數據結構測試(不經常使用):用來保證接口中的數據結構是正確的
- 好比變量有無初始值
- 變量是否溢出
- 邊界條件測試
- 變量沒有賦值(即爲NULL)
- 變量是數值(或字符)
- 主要邊界:最小值,最大值,無窮大(對於DOUBLE等)
- 溢出邊界(指望異常或拒絕服務):最小值-1,最大值+1
- 臨近邊界:最小值+1,最大值-1
- 變量是字符串
- 引用「字符變量」的邊界
- 空字符串
- 對字符串長度應用「數值變量」的邊界
- 變量是集合
- 空集合
- 對集合的大小應用「數值變量」的邊界
- 調整次序:升序、降序
- 變量有規律
- 好比對於Math.sqrt,給出n^2-1,和n^2+1的邊界
- 全部獨立執行通路測試:保證每一條代碼,每一個分支都通過測試
- 代碼覆蓋率
- 語句覆蓋:保證每個語句都執行到了
- 斷定覆蓋(分支覆蓋):保證每個分支都執行到
- 條件覆蓋:保證每個條件都覆蓋到true和false(即if、while中的條件語句)
- 路徑覆蓋:保證每個路徑都覆蓋到
- 相關軟件
- Cobertura:語句覆蓋
- Emma: Eclipse插件Eclemma
- 各條錯誤處理通路測試:保證每個異常都通過測試
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] {}的形式,而且不能存在依賴關係,由於測試方法的調用順序是不可預知的。
上例執行後,控制檯會輸出 socket
- Set up
- Test Math.pow
- Tear down
- Set up
- Test Math.min
- Tear down
從中,能夠猜想到,對於每一個測試方法,調用的形式是: ide
- 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會自動調用此測試方法,如 post
- 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;
- }
- }
容許上述方法,控制檯輸出
寫道
create 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經過註解的方式來識別測試方法。目前支持的主要註解有:
- @BeforeClass 全局只會執行一次,並且是第一個運行
- @Before 在測試方法運行以前運行
- @Test 測試方法
- @After 在測試方法運行以後容許
- @AfterClass 全局只會執行一次,並且是最後一個運行
- @Ignore 忽略此方法
下面舉一個樣例:
- 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 before class
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類中定義了不少靜態方法來進行斷言。列表以下:
- 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有:
其中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();
- }