天天看點

Java單元測試(Junit+Mock+代碼覆寫率)

單元測試是編寫測試代碼,用來檢測特定的、明确的、細顆粒的功能。單元測試并不一定保證程式功能是正确的,更不保證整體業務是準備的。

單元測試不僅僅用來保證目前代碼的正确性,更重要的是用來保證代碼修複、改進或重構之後的正确性。

一般來說,單元測試任務包括

接口功能測試:用來保證接口功能的正确性。

局部資料結構測試(不常用):用來保證接口中的資料結構是正确的

比如變量有無初始值

變量是否溢出

邊界條件測試

變量沒有指派(即為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單元測試(Junit+Mock+代碼覆寫率)

// 測試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>的形式,并且不能存在依賴關系,因為測試方法的調用順序是不可預知的。

上例執行後,控制台會輸出

Java單元測試(Junit+Mock+代碼覆寫率)

Set up  

Test Math.pow  

Tear down  

Test Math.min  

從中,可以猜測到,對于每個測試方法,調用的形式是:

Java單元測試(Junit+Mock+代碼覆寫率)

testCase.setUp();  

testCase.testXXX();  

testCase.tearDown();     

在Eclipse中,可以直接在類名或測試方法上右擊,在彈出的右擊菜單中選擇Run As -&gt; JUnit Test。

在Mvn中,可以直接通過<code>mvn test</code>指令運作測試用例。

也可以通過Java方式調用,建立一個<code>TestCase</code>執行個體,然後重載<code>runTest()</code>方法,在其方法内調用測試方法(可以多個)。

Java單元測試(Junit+Mock+代碼覆寫率)

TestCase test = new Junit3TestCase("mathPow") {  

        // 重載  

    protected void runTest() throws Throwable {  

        testMathPow();  

    };  

};  

test.run();  

更加便捷地,可以在建立<code>TestCase</code>執行個體時直接傳入測試方法名稱,JUnit會自動調用此測試方法,如

Java單元測試(Junit+Mock+代碼覆寫率)

TestCase test = new Junit3TestCase("testMathPow");  

TestSuite是測試用例套件,能夠運作過個測試方法。如果不指定TestSuite,會建立一個預設的TestSuite。預設TestSuite會掃描目前内中的所有測試方法,然後運作。

如果不想采用預設的TestSuite,則可以自定義TestSuite。在TestCase中,可以通過靜态方法<code>suite()</code>傳回自定義的suite。

Java單元測試(Junit+Mock+代碼覆寫率)

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,如

Java單元測試(Junit+Mock+代碼覆寫率)

// 先建立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> 忽略此方法

下面舉一個樣例:

Java單元測試(Junit+Mock+代碼覆寫率)

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> -&gt; <code>@Before</code> -&gt; <code>@Test</code> -&gt; <code>@After</code> -&gt; <code>@Before</code> -&gt; <code>@Test</code> -&gt; <code>@After</code> -&gt; <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異常(可選)。

驗證期望過程将會檢查方法的調用次數。

一個簡單的樣例是:

Java單元測試(Junit+Mock+代碼覆寫率)

@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還支援嚴格的檢查,要求執行的方法次序與期望的完全一緻。

這裡從官方樣例中摘錄幾個典型的:

驗證調用行為

Java單元測試(Junit+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

Java單元測試(Junit+Mock+代碼覆寫率)

//也可以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來分析。

Java單元測試(Junit+Mock+代碼覆寫率)

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();  

繼續閱讀