單元測試是編寫測試代碼,用來檢測特定的、明确的、細顆粒的功能。單元測試并不一定保證程式功能是正确的,更不保證整體業務是準備的。
單元測試不僅僅用來保證目前代碼的正确性,更重要的是用來保證代碼修複、改進或重構之後的正确性。
一般來說,單元測試任務包括
- 接口功能測試:用來保證接口功能的正确性。
- 局部資料結構測試(不常用):用來保證接口中的資料結構是正确的
- 比如變量有無初始值
- 變量是否溢出
- 邊界條件測試
- 變量沒有指派(即為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代碼

- // 測試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運作之後運作
- protected void tearDown() throws Exception {
- System.out.println("Tear down");
- }
如果采用預設的TestSuite,則測試方法必須是
public void testXXX() [throws Exception] {}
的形式,并且不能存在依賴關系,因為測試方法的調用順序是不可預知的。
上例執行後,控制台會輸出
Text代碼

- Set up
- Test Math.pow
- Tear down
- Test Math.min
從中,可以猜測到,對于每個測試方法,調用的形式是:

- 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");
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 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 {
- @Test
- Assert.assertEquals(4.0, Math.pow(2.0, 2.0), 0.0);
- 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
- public void testMathMax() {
- Assert.fail("沒有實作");
- // 使用“假設”來忽略測試方法
- public void testAssume(){
- System.out.println("Test assume");
- // 當假設失敗時,則會停止運作,但這并不會意味測試方法失敗。
- Assume.assumeTrue(false);
- Assert.fail("沒有實作");
- @After
- public void tearDown() throws Exception {
- @AfterClass
- public static void tearDownAfterClass() {
- 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異常(可選)。
驗證期望過程将會檢查方法的調用次數。
一個簡單的樣例是:

- @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
- // 執行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();
出處:javascript:void(0)
原文見此處
TestCase
TestCase
@Test

public void testXXX() [throws Exception] {}


mvn test
TestCase
runTest()

TestCase

suite()

testMathPow
testMathMin

runCount
-
@BeforeClass
-
@Before
-
@Test
-
@After
-
@AfterClass
-
@Ignore

junit.framework
org.junit
@BeforeClass
@Before
@Test
@After
@Before
@Test
@After
@AfterClass
@Ignore
mvn test

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