原文見此處
單元測試是編寫測試代碼,用來檢測特定的、明確的、細顆粒的功能。單元測試並不一定保證程序功能是正確的,更不保證整體業務是准備的。
單元測試不僅僅用來保證當前代碼的正確性,更重要的是用來保證代碼修復、改進或重構之后的正確性。
一般來說,單元測試任務包括
- 接口功能測試:用來保證接口功能的正確性。
- 局部數據結構測試(不常用):用來保證接口中的數據結構是正確的
- 比如變量有無初始值
- 變量是否溢出
- 邊界條件測試
- 變量沒有賦值(即為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] {}
的形式,並且不能存在依賴關系,因為測試方法的調用順序是不可預知的。
上例執行后,控制台會輸出
- 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通過注解的方式來識別測試方法。目前支持的主要注解有:
@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
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();
- }