單元測試是編寫測試代碼,用來檢測特定的、明确的、細顆粒的功能。單元測試并不一定保證程式功能是正确的,更不保證整體業務是準備的。
單元測試不僅僅用來保證目前代碼的正确性,更重要的是用來保證代碼修複、改進或重構之後的正确性。
一般來說,單元測試任務包括
接口功能測試:用來保證接口功能的正确性。
局部資料結構測試(不常用):用來保證接口中的資料結構是正确的
比如變量有無初始值
變量是否溢出
邊界條件測試
變量沒有指派(即為NULL)
變量是數值(或字元)
主要邊界:最小值,最大值,無窮大(對于DOUBLE等)
溢出邊界(期望異常或拒絕服務):最小值-1,最大值+1
臨近邊界:最小值+1,最大值-1
變量是字元串
引用“字元變量”的邊界
空字元串
對字元串長度應用“數值變量”的邊界
變量是集合
空集合
對集合的大小應用“數值變量”的邊界
調整次序:升序、降序
變量有規律比如對于Math.sqrt,給出n^2-1,和n^2+1的邊界
所有獨立執行通路測試:保證每一條代碼,每個分支都經過測試
代碼覆寫率
語句覆寫:保證每一個語句都執行到了
判定覆寫(分支覆寫):保證每一個分支都執行到
條件覆寫:保證每一個條件都覆寫到true和false(即if、while中的條件語句)
路徑覆寫:保證每一個路徑都覆寫到
相關軟體
Cobertura:語句覆寫
Emma: Eclipse插件Eclemma
各條錯誤處理通路測試:保證每一個異常都經過測試
JUnit是Java單元測試架構,已經在Eclipse中預設安裝。目前主流的有JUnit3和JUnit4。JUnit3中,測試用例需要繼承<code>TestCase</code>類。JUnit4中,測試用例無需繼承<code>TestCase</code>類,隻需要使用<code>@Test</code>等注解。
先看一個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運作之後運作
protected void tearDown() throws Exception {
System.out.println("Tear down");
}
如果采用預設的TestSuite,則測試方法必須是<code>public void testXXX() [throws Exception] {}</code>的形式,并且不能存在依賴關系,因為測試方法的調用順序是不可預知的。
上例執行後,控制台會輸出

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

testCase.setUp();
testCase.testXXX();
testCase.tearDown();
在Eclipse中,可以直接在類名或測試方法上右擊,在彈出的右擊菜單中選擇Run As -> JUnit Test。
在Mvn中,可以直接通過<code>mvn test</code>指令運作測試用例。
也可以通過Java方式調用,建立一個<code>TestCase</code>執行個體,然後重載<code>runTest()</code>方法,在其方法内調用測試方法(可以多個)。

TestCase test = new Junit3TestCase("mathPow") {
// 重載
protected void runTest() throws Throwable {
testMathPow();
};
};
test.run();
更加便捷地,可以在建立<code>TestCase</code>執行個體時直接傳入測試方法名稱,JUnit會自動調用此測試方法,如

TestCase test = new Junit3TestCase("testMathPow");
TestSuite是測試用例套件,能夠運作過個測試方法。如果不指定TestSuite,會建立一個預設的TestSuite。預設TestSuite會掃描目前内中的所有測試方法,然後運作。
如果不想采用預設的TestSuite,則可以自定義TestSuite。在TestCase中,可以通過靜态方法<code>suite()</code>傳回自定義的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
并且隻運作了<code>testMathPow</code>測試方法,而沒有運作<code>testMathMin</code>測試方法。通過顯式指定測試方法,可以控制測試執行的順序。
也可以通過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中儲存了很多測試資料,包括運作測試方法數目(<code>runCount</code>)等。
與JUnit3不同,JUnit4通過注解的方式來識别測試方法。目前支援的主要注解有:
<code>@BeforeClass</code> 全局隻會執行一次,而且是第一個運作
<code>@Before</code> 在測試方法運作之前運作
<code>@Test</code> 測試方法
<code>@After</code> 在測試方法運作之後允許
<code>@AfterClass</code> 全局隻會執行一次,而且是最後一個運作
<code>@Ignore</code> 忽略此方法
下面舉一個樣例:

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是<code>junit.framework</code>,而Junit4是<code>org.junit</code>。
執行此用例後,控制台會輸出
Set up before class
Test Math.min
Test exception
Test assume
Tear down After class
可以看到,執行次序是<code>@BeforeClass</code> -> <code>@Before</code> -> <code>@Test</code> -> <code>@After</code> -> <code>@Before</code> -> <code>@Test</code> -> <code>@After</code> -> <code>@AfterClass</code>。<code>@Ignore</code>會被忽略。
與Junit3類似,可以在Eclipse中運作,也可以通過<code>mvn test</code>指令運作。
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測重于對功能的測試重制。比如對于List接口,Mock會直接對List進行模拟,而Stub會建立一個實作了List的TestList,在其中編寫測試的代碼。
強烈建議優先選擇Mock方式,因為Mock方式下,模拟代碼與測試代碼放在一起,易讀性好,而且擴充性、靈活性都比Stub好。
比較流行的Mock有:
<a target="_blank" href="http://jmock.org/">JMock</a>
<a target="_blank" href="http://www.easymock.org/">EasyMock</a>
<a target="_blank" href="http://blog.thihy.info/post/mockito.googlecode.com">Mockito</a>
<a target="_blank" href="http://code.google.com/p/powermock/">powermock</a>
其中EasyMock和Mockito對于Java接口使用接口代理的方式來模拟,對于Java類使用繼承的方式來模拟(也即會建立一個新的Class類)。Mockito支援spy方式,可以對執行個體進行模拟。但它們都不能對靜态方法和final類進行模拟,powermock通過修改位元組碼來支援了此功能。
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還支援嚴格的檢查,要求執行的方法次序與期望的完全一緻。
這裡從官方樣例中摘錄幾個典型的:
驗證調用行為

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);
Jacoco可以嵌入到Ant、Maven中,也可以使用Java Agent技術監控任意Java程式,也可以使用Java Api來定制功能。
Jacoco會監控JVM中的調用,生成監控結果(預設儲存在jacoco.exec檔案中),然後分析此結果,配合源代碼生成覆寫率報告。需要注意的是:監控和分析這兩步,必須使用相同的Class檔案,否則由于Class不同,而無法定位到具體的方法,導緻覆寫率均為0%。
可以使用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();