8點了解Java服務端單元測試

一. 前言

單元測試並不僅是爲了驗證你當前所寫的代碼是否存在問題,更爲重要的是它能夠很大程度的保障往後因業務變動、修復Bug或重構等引發的代碼變動而致使(或新增)的風險。css

同時將單元測試提早到編寫正式代碼進行(測試驅動開發),能夠很好的提升對代碼結構的設計。經過優先編寫測試用例,能夠很好的從用戶角度來對功能的分解、使用過程和接口等進行設計,從而提升代碼結構的高內聚、低耦合特性。使得對往後的需求變動或代碼重構等更加高效、簡潔。java

所以編寫單元測試對產品開發和維護、技術提高和積累具備重大意義!面試

二. 第一個單元測試

首先寫一個單元測試,這樣有助於對後面內容的理解與實踐。算法

2.1 開發環境

**IntelliJ IDEA **
IntelliJ IDEA默認自帶並啓用TestNG和覆蓋率插件:spring

  • TestNG

在設置窗口查看TestNG插件是否安裝與啓用:apache

 

  • 覆蓋率

一樣,查看覆蓋率插件能夠搜索「Coverage」。IntelliJ IDEA的覆蓋率統計工具備三種,JaCoCo、Emma和IntelliJ IDEA自帶。api

 

  • 變異測試

一樣,查看並安裝變異測試插件能夠搜索「PIT mutation testing」。數組


**Eclipse **
Eclipse須要自行安裝單元測試相關插件:tomcat

  • TestNG

執行TestNG單元測試的插件。可在Eclipse Marketplace搜索「TestNG」安裝:安全

  • 覆蓋率

獲取單元測試覆蓋率的插件。可在Eclipse Marketplace搜索「EclEmma」安裝:

  • 變異測試

一樣,查看並安裝變異測試插件能夠搜索「Pitclipse」。

2.2 Maven依賴

  • TestNG
<dependency>
   <groupId>org.testng</groupId>
   <artifactId>testng</artifactId>
   <version>${testng.version}</version>
   <scope>test</scope>
</dependency>
  • JMockit
<dependency>
   <groupId>org.jmockit</groupId>
   <artifactId>jmockit</artifactId>
   <version>${jmockit.version}</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.jmockit</groupId>
   <artifactId>jmockit-coverage</artifactId>
   <version>${jmockit.version}</version>
   <scope>test</scope>
</dependency>
  • Spring Test
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-test</artifactId>
   <version>${spring.version}</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.kubek2k</groupId>
   <artifactId>springockito</artifactId>
   <version>${springockito.version}</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.kubek2k</groupId>
   <artifactId>springockito-annotations</artifactId>
   <version>${springockito.version}</version>
   <scope>test</scope>
</dependency>
  • 其餘(或許須要)
<dependency>
   <groupId>org.apache.tomcat</groupId>
   <artifactId>tomcat-servlet-api</artifactId>
   <version>${tomcat.servlet.api.version}</version>
   <scope>test</scope>
</dependency>

2.3 建立單元測試

下面介紹經過IDE自動建立單元測試的方法(也可手動完成):
IntelliJ IDEA

Eclipse:

2.在彈出的窗口中搜索「Test」,選擇「TestNG class」後點擊「Next」按鈕:

3.在窗口中選擇要建立的測試方法後點擊「Next」按鈕:

4.根據本身的狀況設置包名、類名和Annotations等:

示例代碼
可參考下例代碼編寫單元測試:

package org.light4j.unit.test;

import mockit.Expectations;
import mockit.Injectable;
import mockit.Tested;
import org.testng.Assert;
import org.testng.annotations.Test;
import wow.unit.test.remote.UserService;
import java.util.List;

/**
 * 單元測試demo
 *
 * @author jiazuo.ljz
 */
public class BookServiceTest {

    /**
     * 圖書持久化類,遠程接口
     */
    @Injectable
    private BookDAO bookDAO;

    /**
     * 用戶服務,遠程接口
     */
    @Injectable
    private UserService userService;

    /**
     * 圖書服務,本地接口
     */
    @Tested(availableDuringSetup = true)
    private BookService bookService;

    /**
     * 測試根據用戶的Nick查詢用戶的圖書列表方法
     * 其中「getUserBooksByUserNick」方法最終須要經過UserID查詢DB,
     * 因此在調用此方法以前須要先對UserService類的getUserIDByNick方法進行Mock。
     */
    @Test
    public void testGetUserBooksByUserNick() throws Exception {
        new Expectations() {
            {
                userService.getUserIDByNick(anyString); // Mock接口
                result = 1234567; // Mock接口的返回值
                times = 1; // 此接口會被調用一次
            }
        };
        List<BookDO> bookList = bookService.getUserBooksByUserNick("moyuan.jcc");
        Assert.assertNotNull(bookList);
    }
}

2.4 運行單元測試

IntelliJ IDEA

Eclipse

注:也可點擊工具欄選項運行,從左至右依次是:覆蓋率、調試、運行運行。
2.點擊「運行」:
左側框:單元測試運行結果
底側框:單元測試打印輸出的內容

Maven

  • 執行目錄下全部單元測試,進入工程目錄後執行:mvn test
  • 執行具體的單元測試類,多個測試類可用逗號分開:mvn test -Dtest=Test1,Test2
  • 執行具體的單元測試類的方法:mvn test -Dtest=Test1#testMethod
  • 執行某個包下的單元測試:mvn test -Dtest=com/alibaba/biz/*
  • 執行ANT風格路徑表達式下的單元測試:mvn test -Dtest=/Test或mvn test -Dtest=*/???Test
  • 忽略單元測試:mvn -Dmaven.test.skip=true

2.5 單元測試覆蓋

IntelliJ IDEA

Eclipse

2.輸出報告
運行過程以及結果輸出的窗口中有一行「JMockit: Coverage report written to」,是EclEmma建立的覆蓋率報告文件目錄:

覆蓋率報告

2.6 變異測試

變異測試是覆蓋率的一個很好的補充。相比覆蓋率,它可以使單元測試更加健壯。(具體可見5.4節)
IntelliJ IDEA

3. 輸出報告
運行過程以及結果輸出的窗口中最後一行「Open report in browser」即爲插件建立的報告鏈接。
點擊便可打開報告:

Eclipse

2. 輸出報告
可在此窗口中查看變異測試發現的可能存在的代碼缺陷:(這點比IDEA的PIT插件作的要好)
可在此窗口中查看測試報告:

爲從此更好的開展與落實單元測試,請繼續閱讀下面內容。

3 單元測試框架

3.1 TestNG

Junit4TestNGJava很是流行的單元測試框架。因TestNG更加簡潔、靈活和功能豐富,因此咱們選用TestNG
下面經過與Junit4的比較來了解一下TestNG的特性:

註解支持

Junit4TestNG的註解對比:

特性 JUnit4 TestNG
測試註解 @Test @Test
在測試套件執行以前執行 @BeforeSuite
在測試套件執行以後執行 @AfterSuite
在測試以前執行 @BeforeTest
在測試以後執行 @AfterTest
在測試組執行以前執行 @BeforeGroups
在測試組執行以後執行 @AfterGroups
在測試類執行以前執行 @BeforeClass @BeforeClass
在測試類執行以後執行 @AfterClass @AfterClass
在測試方法執行以前執行 @Before @BeforeMethod
在測試方法執行以後執行 @After @AfterMethod
忽略測試 @ignore @Test(enbale=false)
預期異常 @Test(expected = Exception.class) @Test(expectedExceptions = Exception.class)
超時 @Test(timeout = 1000) @Test(timeout = 1000)

// TODO 測試 測試方法 測試套件 測試組 的區別
Junit4中,@BeforeClass@AfterClass只能用於靜態方法。TestNG無此約束。

異常測試

異常測試是指在單元測試中應該要拋出什麼異常是合理的。

  • JUnit4
@Test(expected = ArithmeticException.class)
public void divisionWithException() {
int i = 1/0;
}
  • TestNG
@Test(expectedExceptions = ArithmeticException.class)
public void divisionWithException() {
int i = 1/0;
}

忽略測試

忽略測試是指這個單元測試能夠被忽略。

  • JUnit4
@Ignore("Not Ready to Run")
@Test
public void divisionWithException() {
System.out.println("Method is not ready yet");
}
  • TestNG
@Test(enabled=false)
public void divisionWithException() {
System.out.println("Method is not ready yet");
}

時間測試

時間測試是指一個單元測試運行的時間超過了指定時間(毫秒數),那麼測試將失敗。

  • JUnit4
@Test(timeout = 1000)
public void infinity() {
while (true);
}
  • TestNG
@Test(timeOut = 1000)
public void infinity() {
while (true);
}

套件測試

套件測試是指把多個單元測試組合成一個模塊,而後統一運行。

  • JUnit4

@RunWith@Suite註解被用於執行套件測試。下面的代碼是所展現的是在「JunitTest5」被執行以後須要「JunitTest1」和「JunitTest2」也一塊兒執行。全部的聲明須要在類內部完成。
java

 @RunWith(Suite.class) @Suite.SuiteClasses({JunitTest1.class, JunitTest2.class}) 
public class JunitTest5 { 
  • TestNG

是使用XML配置文件來執行套件測試。下面的配置將「TestNGTest1」和「TestNGTest2」一塊兒執行。

<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" > 
<suite name="My test suite">
 <test name="testing">
   <classes>
   <class name="com.fsecure.demo.testng.TestNGTest1" />
   <class name="com.fsecure.demo.testng.TestNGTest2" />
   </classes>
 </test>
</suite> 

TestNG的另外一種方式使用了組的概念,每一個測試方法均可以根據功能特性分配到一個組裏面。例如:

@Test(groups="method1") 
public void testingMethod1() { 
System.out.println("Method - testingMethod1()"); 
} 
@Test(groups="method2") 
public void testingMethod2() { 
System.out.println("Method - testingMethod2()"); 
} 
@Test(groups="method1") 
public void testingMethod1_1() {
 System.out.println("Method - testingMethod1_1()"); 
} 
@Test(groups="method4") 
public void testingMethod4() { 
System.out.println("Method - testingMethod4()");
 }

這是一個有4個方法,3個組(method1, method2 和 method4)的類。使用起來比XML的套件更簡潔。

下面XML文件配置了一個執行組爲methed1的單元測試。

<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" >
<suite name="My test suite">
    <test name="testing">
        <groups>
            <run>
                <include name="method1"/>
            </run>
        </groups>
        <classes>
            <class name="com.fsecure.demo.testng.TestNGTest5_2_0" />
        </classes>
    </test>
</suite>

分組使集成測試更增強大。例如,咱們能夠只是執行全部測試中的組名爲DatabaseFuntion的測試。

參數化測試

參數化測試是指給單元測試傳多種參數值,驗證接口對多種不一樣參數的處理是否正確。

  • JUnit4

@RunWith@Parameter註解用於爲單元測試提供參數值,@Parameters必須返回List,參數將會被做爲參數傳給類的構造函數。

@RunWith(value = Parameterized.class)
public class JunitTest6 {
private int number;
public JunitTest6(int number) {
    this.number = number;
}
@Parameters
public static Collection<Object[]> data() {
    Object[][] data = new Object[][] { { 1 }, { 2 }, { 3 }, { 4 } };
    return Arrays.asList(data);
}
@Test
public void pushTest() {
    System.out.println("Parameterized Number is : " + number);
}
}

它的使用很不方便:一個方法的參數化測試必須定義一個測試類。測試參數經過一個註解爲@Parameters且返回值爲List參數值列表的靜態方法。而後將方法返回值成員經過類的構造函數初始化爲類的成員。最後再將類的成員作爲參數去測試被測試方法。

  • TestNG

使用XML文件或@DataProvider註解兩種方式爲測試提供參數。

XML文件配置參數化測試
方法上添加@Parameters註解,參數數據由TestNG的XML配置文件提供。這樣作以後,咱們可使用不一樣的數據集甚至是不一樣的結果集來重用一個測試用例。另外,甚至是最終用戶,QA或者QE能夠提供他們本身的XML文件來作測試。

public class TestNGTest6_1_0 {
    @Test
    @Parameters(value="number")
    public void parameterIntTest(int number) {
        System.out.println("Parameterized Number is : " + number);
    }
}

XML 文件

<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" >
<suite name="My test suite">
    <test name="testing">
        <parameter name="number" value="2"/>
        <classes>
            <class name="com.fsecure.demo.testng.TestNGTest6_0" />
        </classes>
    </test>
</suite>

@DataProvider註解參數化測試
使用XML文件初始化數據雖然方便,但僅支持基礎數據類型。如需複雜的類型可以使用@DataProvider註解解決。

@Test(dataProvider = "Data-Provider-Function")
public void parameterIntTest(Class clzz, String[] number) {
    System.out.println("Parameterized Number is : " + number[0]);
    System.out.println("Parameterized Number is : " + number[1]);
}
//This function will provide the patameter data
@DataProvider(name = "Data-Provider-Function")
public Object[][] parameterIntTestProvider() {
    return new Object[][]{
    {Vector.class, new String[]{"java.util.AbstractList",   "java.util.AbstractCollection"}},
    {String.class, new String[] {"1", "2"}},
    {Integer.class, new String[] {"1", "2"}}
};
}

@DataProvider做爲對象的參數
P.S 「TestNGTest6_3_0」 是一個簡單的對象,使用了get和set方法。

@Test(dataProvider = "Data-Provider-Function")
public void parameterIntTest(TestNGTest6_3_0 clzz) {
    System.out.println("Parameterized Number is : " + clzz.getMsg());
    System.out.println("Parameterized Number is : " + clzz.getNumber());
}
//This function will provide the patameter data
@DataProvider(name = "Data-Provider-Function")
public Object[][] parameterIntTestProvider() {
    TestNGTest6_3_0 obj = new TestNGTest6_3_0();
    obj.setMsg("Hello");
    obj.setNumber(123);
    return new Object[][]{{obj}};
}

TestNG的參數化測試使用起來很是方便,它能夠在一個測試類中添加多個方法的參數化測試(JUnit4一個方法就須要一個類)。

依賴測試

依賴測試是指測試的方法是有依賴的,在執行的測試以前須要執行的另外一測試。若是依賴的測試出現錯誤,全部的子測試都被忽略,且不會被標記爲失敗。

  • JUnit4

JUnit4框架主要聚焦於測試的隔離,暫時還不支持這個特性。

  • TestNG

它使用dependOnMethods來實現了依賴測試的功能,以下:

@Test
public void method1() {
System.out.println("This is method 1");
}
@Test(dependsOnMethods={"method1"})
public void method2() {
System.out.println("This is method 2");
}

若是method1()成功執行,那麼method2()也將被執行,不然method2()將會被忽略。

性能測試

TestNG支持經過多個線程併發調用一個測試接口來實現性能測試。JUnit4不支持,若要進行性能測試需手動添加併發代碼。

@Test(invocationCount=1000, threadPoolSize=5, timeOut=100)
public void perfMethod() {
    System.out.println("This is perfMethod");
}

並行測試

TestNG支持經過多個線程併發調用多個測試接口執行測試,相對於傳統的單線程執行測試的方式,能夠很大程度減小測試運行時間。

public class ConcurrencyTest {
    @Test
    public void method1() {
        System.out.println("This is method 1");
    }
    @Test
    public void method2() {
        System.out.println("This is method 2");
    }
}

並行測試配置:

<suite name="Concurrency Suite" parallel="methods" thread-count="2" >
  <test name="Concurrency Test" group-by-instances="true">
    <classes>
      <class name="wow.unit.test.ConcurrencyTest" />
    </classes>
  </test>
</suite>

討論總結

經過上面的對比,建議使用TestNG做爲Java項目的單元測試框架,由於TestNG在參數化測試、依賴測試以、套件測試(組)及併發測試方面功能更加簡潔、強大。另外,TestNG也涵蓋了JUnit4的所有功能。

3.2 JMockit

Mock的使用場景:

好比Mock如下場景:

      1. 外部依賴的應用的調用,好比WebService等服務依賴。
      2. DAO層(訪問MySQL、Oracle、Emcache等底層存儲)的調用等。
      3. 系統間異步交互通知消息。
      4. methodA裏面調用到的methodB。
      5. 一些應用裏面本身的Class(abstract,final,static)、Interface、Annotation、Enum和Native等。

Mock工具的原理:

Mock工具工做的原理大都以下:

      1. Record階段:錄製指望。也能夠理解爲數據準備階段。建立依賴的Class或Interface或Method,模擬返回的數據、耗時及調用的次數等。
      2. Replay階段:經過調用被測代碼,執行測試。期間會Invoke到第一階段Record的Mock對象或方法。
      3. Verify階段:驗證。能夠驗證調用返回是否正確,及Mock的方法調用次數,順序等。

當前的一些Mock工具的比較:

歷史曾經或當前比較流行的Mock工具備EasyMockjMockMockitoUnitils MockPowerMockJMockit等工具。
從這裏能夠看到,JMockit的的功能最全面、強大!因此咱們單元測試中的Mock工具也選擇了JMockit。同時在開發的過程當中,JMockit的「Auto-injection of mocks」及「Special fields for 「any」 argument matching」及各類有用的Annotation使單元測試的開發更簡潔和高效。

JMockit的簡介:

JMockit是用以幫助開發人員編寫單元測試的Mock工具。它基於java.lang.instrument包開發,並使用ASM庫來修改Java的Bytecode。正所以兩點,它能夠實現無所不能的Mock。

JMockit能夠Mock的種類包含了:

  • class(abstract, final, static)
  • interface
  • enum
  • annotation
  • native

JMockit有兩種Mock的方式:

  • Behavior-oriented(Expectations & Verifications)
  • State-oriented(MockUp)

通俗點講,Behavior-oriented是基於行爲的Mock,對Mock目標代碼的行爲進行模仿,像是黑盒測試。State-oriented是基於狀態的Mock,是站在目標測試代碼內部的。能夠對傳入的參數進行檢查、匹配,才返回某些結果,相似白盒。而State-oriented的new MockUp基本上能夠Mock任何代碼或邏輯。

如下是JMockit的APIs和tools:

能夠看到JMockit經常使用的Expectation、StrictExpectations和NonStrictExpectations指望錄製及註解@Tested、@Mocked,@NonStrict、@Injectable等簡潔的Mock代碼風格。並且JMockit還自帶了Code Coverage的工具供本地單元測試時候邏輯覆蓋或代碼覆蓋率使用。

JMockit的使用:

以「第一個單元測試」代碼爲例:

  • 測試對象

@Tested:JMockit會自動建立註解爲「@Tested」的類對象,並將其作爲被測試對象。 經過設置「availableDuringSetup=true」參數,可使得被測試對象在「setUp」方法執行前被建立出來。

@Tested(availableDuringSetup = true)
private BookService bookService;
  • Mock對象

@Injectable:JMockit自動建立註解爲「@Injectable」的類對象,並將其自動注入被測試對象。

@Injectable
private BookDAO bookDAO;
@Injectable
private UserService userService;

相關的註解還有:// TODO 待補充

  • 錄製

Expectations:塊裏的內容是用來Mock方法,並指定方法的返回值、異常、調用次數和耗時。此塊中的方法是必須被執行的,不然單元測試失敗。

/**
* 測試根據用戶的Nick查詢用戶的圖書列表方法
* 其中「getUserBooksByUserNick」方法最終須要經過UserId查詢DB,
* 因此在調用此方法以前須要先對UserService類的getUserIdByNick方法進行Mock。
*/
@Test
public void testGetUserBooksByUserNick() throws Exception {
new Expectations() {
{
  userService.getUserIdByNick(anyString);
  result = 1234567;
  times = 1;
}
};
List<BookDO> bookList = bookService.getUserBooksByUserNick("moyuan.jcc");
Assert.assertNotNull(bookList);
}

相關的類還有:

  • 結果驗證

Assert:是最多見的斷言驗證

Assert.assertNotNull(bookList); 

Verifications:一種特殊的驗證塊。好比:要驗證一個被測試類中,調用的某個方法是否爲指定的參數、調用次數。相比Expectations它放在單元測試的最後且沒有Mock功能。

注:以上列舉的註釋具體用法示例請查閱第7節內容

4 單元測試內容

在單元測試時,測試人員根據設計文檔和源碼,瞭解模塊的接口和邏輯結構。主要採用白盒測試用例,輔之黑盒測試用例,使之對任何(合理和不合理)的輸入都要能鑑別和響應。這就要求對程序全部的局部和全局的數據結構、外部接口和程序代碼的關鍵部分進行檢查。

在單元測試中主要在5個方面對被測模塊進行檢查。

4.1 模塊接口測試

在單元測試開始時,應該對全部被測模塊的接口進行測試。若是數據不能正常地輸入和輸出,那麼其餘的測試毫無心義。Myers在關於軟件測試的書中爲接口測試提出了一個檢查表:

  • 模塊輸入參數的數目是否與模塊形式參數數目相同
  • 模塊各輸入的參數屬性與對應的形參屬性是否一致
  • 模塊各輸入的參數類型與對應的形參類型是否一致
  • 傳到被調用模塊的實參的數目是否與被調用模塊形參的數目相同
  • 傳到被調用模塊的實參的屬性是否與被調用模塊形參的屬性相同
  • 傳到被調用模塊的實參的類型是否與被調用模塊形參的類型相同
  • 引用內部函數時,實參的次序和數目是否正確
  • 是否引用了與當前入口無關的參數
  • 用於輸入的變量有沒有改變
  • 在通過不一樣模塊時,全局變量的定義是否一致
  • 限制條件是否以形參的形式傳遞
  • 使用外部資源時,是否檢查可用性並及時釋放資源,如內存、文件、硬盤、端口等

當模塊經過外部設備進行輸入/輸出操做時,必須擴展接口測試,附加以下的測試項目:

  • 文件的屬性是否正確
  • Open與Close語句是否正確
  • 規定的格式是否與I/O語句相符
  • 緩衝區的大小與記錄的大小是否相配合
  • 在使用文件前,文件是否打開
  • 文件結束的條件是否會被執行
  • I/O錯誤是否檢查並作了處理
  • 在輸出信息中是否有文字錯誤

4.2 局部數據結構測試

模塊的局部數據結構是最多見的錯誤來源,應設計測試用例以檢查如下各類錯誤:

  • 不正確或不一致的數據類型說明
  • 使用還沒有賦值或還沒有初始化的變量
  • 錯誤的初始值或錯誤的默認值
  • 變量名拼寫錯或書寫錯——使用了外部變量或函數
  • 不一致的數據類型
  • 全局數據對模塊的影響
  • 數組越界
  • 非法指針

4.3 路徑測試

檢查因爲計算、斷定和控制流錯誤而致使的程序錯誤。因爲在測試時不可能作到窮舉測試,因此在單元測試時要根據「白盒」測試和「黑盒」測試用例的設計方法設計測試用例,對模塊中重要的執行路徑進行測試。重要的執行路徑是一般指那些處在具體實現的算法、控制、數據處理等重要位置的路徑,也可指較複雜而容易出錯的路徑。儘量地對執行路徑進行測試很是重要,須要設計因錯誤的計算、比較或控制流而致使錯誤的測試用例。此外,對基本執行路徑和循環進行測試也可發現大量的路徑錯誤。

在路徑測試中,要檢查的錯誤有:死代碼、錯誤的計算優先級、算法錯誤、混用不一樣類的操做、初始化不正確、精度錯誤——比較運算錯誤、賦值錯誤、表達式的不正確符號——>、>=;=、==、!=和循環變量的使用錯誤——錯誤賦值以及其餘錯誤等。

比較操做和控制流向緊密相關,測試用例設計須要注意發現比較操做的錯誤:

  • 不一樣數據類型的比較(注意包裝類與基礎類型的比較)
  • 不正確的邏輯運算符或優先次序
  • 因浮點運算精度問題而形成的兩值比較不等
  • 關係表達式中不正確的變量和比較符
  • 「差1錯」,即不正常的或不存在的循環中的條件
  • 當遇到發散的循環時沒法跳出循環
  • 當遇到發散的迭代時不能終止循環
  • 錯誤的修改循環變量

4.4 錯誤處理測試

錯誤處理路徑是指可能出現錯誤的路徑以及進行錯誤處理的路徑。當出現錯誤時會執行錯誤處理代碼,或通知用戶處理,或中止執行並使程序進入一種安全等待狀態。測試人員應意識到,每一行程序代碼均可能執行到,不能自認爲錯誤發生的機率很小而不進行測試。通常軟件錯誤處理測試應考慮下面幾種可能的錯誤:

  • 出錯的描述是否難以理解,是否可以對錯誤定位
  • 顯示的錯誤與實際的錯誤是否相符
  • 對錯誤條件的處理正確與否
  • 在對錯誤進行處理以前,錯誤條件是否已經引發系統的干預等

在進行錯誤處理測試時,要檢查以下內容:

  • 在資源使用先後或其餘模塊使用先後,程序是否進行錯誤出現檢查
  • 出現錯誤後,是否能夠進行錯誤處理,如引起錯誤、通知用戶、進行記錄
  • 在系統干預前,錯誤處理是否有效,報告和記錄的錯誤是否真實詳細

4.5 邊界測試

邊界測試是單元測試中最後的任務。代碼經常在邊界上出錯,好比:在代碼段中有一個n次循環,當到達第n次循環時就可能會出錯;或者在一個有n個元素的數組中,訪問第n個元素時是很容易出錯的。所以,要特別注意數據流、控制流中恰好等於、大於或小於肯定的比較值時可能會出現的錯誤。對這些地方須要仔細地認真加以測試。

此外,若是對模塊性能有要求的話,還要專門對關鍵路徑進行性能測試。以肯定最壞狀況下和平均意義下影響運行時間的因素。下面是邊界測試的具體要檢查的內容:

  • 普通合法數據是否正確處理
  • 普通非法數據是否正確處理
  • 邊界內最接近邊界的(合法)數據是否正確處理
  • 邊界外最接近邊界的(非法)數據是否正確處理等
  • 在n次循環的第0次、第1次、第n次是否有錯誤
  • 運算或判斷中取最大最小值時是否有錯誤
  • 數據流、控制流中恰好等於、大於、小於肯定的比較值時是否出現錯誤

5 單元測試規範

5.1 命名規範

5.2 測試內容

第4部分歸納的列舉了須要測試的5大點內容,此處爲服務端代碼層至少要包含或覆蓋的測試內容。
Service

  • 局部數據結構測試
  • 路徑測試
  • 錯誤處理測試
  • 邊界測試

HTTP接口

  • 模擬接口測試
  • 局部數據結構測試
  • 路徑測試
  • 錯誤處理測試
  • 邊界測試

HSF接口

  • 模擬接口測試
  • 局部數據結構測試
  • 路徑測試
  • 錯誤處理測試
  • 邊界測試

工具類

  • 模擬接口測試
  • 局部數據結構測試
  • 路徑測試
  • 錯誤處理測試
  • 邊界測試

5.3 覆蓋率

爲了使單元測試能充分細緻地展開,應在實施單元測試中遵照下述要求:

  1. 語句覆蓋達到100%
    語句覆蓋指被測單元中每條可執行語句都被測試用例所覆蓋。語句覆蓋是強度最低的覆蓋要求,要注重語句覆蓋的意義。好比,用一段從沒執行過的程序控制航天飛機升上天空,而後使它精確入軌,這種行爲的後果不敢想象。實際測試中,不必定能作到每條語句都被執行到。第一,存在「死碼」,即因爲代碼設計錯誤在任何狀況下都不可能執行到的代碼。第二,不是「死碼」,可是因爲要求的輸入及條件很是難達到或單元測試的實現所限,使得代碼沒有獲得執行。所以,在可執行語句未獲得執行時,要深刻程序做作詳細的分析。若是是屬於以上兩種狀況,則能夠認爲完成了覆蓋。可是對於後者,也要儘可能測試到。若是以上二者都不是,則是由於測試用例設計不充分,須要再設計測試用例。

  2. 分支覆蓋達到100%
    分支覆蓋指分支語句取真值和取假值各一次。分支語句是程序控制流的重要處理語句,在不一樣流向上設計能夠驗證這些控制流向正確性的測試用命。分支覆蓋使這些分支產生的輸出都獲得驗證,提升測試的充分性。

  3. 覆蓋錯誤處理路徑
    即異常處理路徑

  4. 單元的軟件特性覆蓋
    軟件的特性包括功能、性能、屬性、設計約束、狀態數目、分支的行數等。

  5. 對試用額定數據值、奇異數據值和邊界值的計算進行檢驗。用假想的數據類型和數據值運行測試,排斥不規則的輸入。

單元測試一般是由編寫程序的人本身完成的,可是項目負責人應當關心測試的結果。全部的測試用例和測試結果都是模塊開發的重要資料,需妥善保存。

5.4 變異測試

測試覆蓋方法的確能夠幫咱們找到一些顯而易見的代碼冗餘或者測試遺漏的問題。不過,實踐證實,這些傳統的方法只能很是有限的發現測試中的問題。不少代碼和測試的問題在覆蓋達到100%的狀況下也沒法發現。然而,「代碼變異測試」這種方法能夠很好的彌補傳統方法的缺點,產生更加有效的單元測試。

代碼變異測試是經過對代碼產生「變異」來幫助咱們改進單元測試的。「變異」指的是修改一處代碼來改變代碼行爲(固然保證語法的合理性)。簡單來講,代碼變異測試先試着對代碼產生這樣的變異,而後運行單元測試,並檢查是否有測試是由於這個代碼變異而失敗。若是失敗,那麼說明這個變異被「消滅」了,這是咱們指望看到的結果。不然說明這個變異「存活」了下來,這種狀況下咱們就須要去研究一下「爲何」了。

總而言之,測試覆蓋這種方法是一種不錯的保障單元測試質量的手段。代碼變異測試則比傳統的測試覆蓋方法能夠更加有效的發現代碼和測試中潛在的問題,它可使單元測試更增強壯。

6 CISE集成

7 單元測試示例

7.1 Service

Service層單元測試示例。
普通Mock測試:

/**
* 測試根據用戶的Nick查詢用戶的圖書列表方法
* 其中「userService.getUserBooksByUserNick」方法最終須要經過UserId查詢DB,
* 因此在調用此方法以前須要先對UserService類的getUserIdByNick方法進行Mock。
* 其中「bookDAO.getUserBooksByUserId」方法最終須要經過UserId查詢DB,
* 因此在調用此方法以前須要先對BookDAO類的getUserBooksByUserId方法進行Mock。
*/
@Test
public void testGetUserBooksByUserNick4Success() throws Exception {
final List<BookDO> bookList = new ArrayList<BookDO>();
bookList.add(new BookDO());
new Expectations() {
{
  userService.getUserIdByNick(anyString); // Mock的接口
  result = 1234567; // 接口返回值
  times = 1; // 接口被調用的次數

  bookDAO.getUserBooksByUserId(anyLong);
  result = bookList;
  times = 1;
}
};
List<BookDO> resultBookList = bookService.getUserBooksByUserNick("moyuan.jcc");
Assert.assertNotNull(resultBookList);
}

2.錯誤(異常)處理:

/**
* 測試根據用戶的Nick查詢用戶的圖書列表方法,注意在@Test添加expectedExceptions參數
* 驗證其中「userService.getUserBooksByUserNick」接口出現異常時,對異常的處理是否符合預期.
* 其中「bookDAO.getUserBooksByUserId」方法不會被調用到。
*/
@Test(expectedExceptions = {RuntimeException.class})
public void testGetUserBooksByUserNick4Exception() throws Exception {
final List<BookDO> bookList = new ArrayList<BookDO>();
bookList.add(new BookDO());
new Expectations() {
{
  userService.getUserIdByNick(anyString); // Mock的接口
  result = new RuntimeException("exception unit test"); // 接口拋出異常
  times = 1; // 接口被調用的次數

  bookDAO.getUserBooksByUserId(anyLong);
  result = bookList;
  times = 0; // 上面接口出現異常後,此接口不會被調用
}
};
List<BookDO> resultBookList = bookService.getUserBooksByUserNick("moyuan.jcc");
Assert.assertNotNull(resultBookList);
}

3. Mock具體方法實現:

/**
* 測試發送離線消息方法
* 消息隊列:當離線消息超過100條時,刪除最舊1條,添加最新一條。
* 但消息存在DB或Tair中,因此須要Mock消息的存儲。
*/ 
@Test
public void testAddOffLineMsg() throws Exception {
final Map<Long, MsgDO> msgCache = new ArrayList<Long, MsgDO>();
new Expectations() {
{
    new MockUp<BookDAO>() {
        @Mock
        public void addMsgByUserId(long userId, MsgDO msgDO) {
           msgCache.put(userId, msgDO);
        }
    };
    new MockUp<BookDAO>() {
        @Mock
        public List<MsgDO> getUserBooksByUserId(long userId) {
           return msgCache.get(userId);
        }
    };
}
};

final int testAddMsgCount = 102;
for(int i = 0; i < testAddMsgCount; i++) {
msgService.addMsgByUserId(123L, new MsgDO(new Date(), "this is msg" + i));
}
List<MsgDO> msgList = msgService.getMsgByUserId(123L);  
Assert.assertTrue(msgList.size() == 100);

new Verifications() {
{
    // 驗證 addMsgByUserId 接口是否被調用了100次
    MsgDAO.addMsgByUserId(anyLong, withInstanceOf(MsgDO.class));
    times = testAddMsgCount;
    // 驗證是否對消息內容進行相就次數的轉義
    SecurityUtil.escapeHtml(anyString);
    times = testAddMsgCount;
}
};
}

7.2 HTTP

HTTP接口單元測試示例。
1. Spring MVC Controller

public final class BookControllerTest {

@Tested(availableDuringSetup = true)
private BookController bookController;

@Injectable
private BookService bookService;

private MockMvc mockMvc;

@BeforeMethod
public void setUp() throws Exception {
this.mockMvc = MockMvcBuilders.standaloneSetup(bookController).build();
}

/**
*<strong>  </strong>********************************
* getBookList unit test
*<strong>  </strong>********************************
*/
@Test
public void testgetBookList4Success() throws Exception {
new StrictExpectations() {
    {
        new MockUp<CookieUtil>(){
            @Mock
            public boolean isLogined(){
                return true;
            }
        };
        userService.getUserBooksByUserNick(anyString);
        result = null;
        times = 1;
    }
};
ResultActions httpResult = this.mockMvc.perform(get("/education/getBookList.do?userNick=hello"))
    .andDo(print()).andExpect(status().isOk());
MockHttpServletResponse response = httpResult.andReturn().getResponse();
String responseStr = response.getContentAsString();
// 若是存在多版本客戶端的狀況下,注意返回值向後兼容,此處須要多種格式驗證.
Assert.assertEquals(responseStr, "{\"code\":1,\"msg\":\"success\",\"data\":\"\"}");
}
}

2. 參數化測試

@DataProvider(name = "getBookListParameterProvider") 
public Object[][] getBookListParameterProvider() {
return new String[][]{
    {"hello", "{\"code\":1,\"msg\":\"success\",\"data\":\"\"}"},
    {"123", "{\"code\":301,\"msg\":\"parameter error\",\"data\":\"\"}"}
};
}
@Test(dataProvider = "getBookListParameterProvider")
public void testgetBookList4Success(String nick ,String resultCheck) throws Exception {
new StrictExpectations() {
    {
        new MockUp<CookieUtil>() {
            @Mock
            public boolean isLogined() {
                return true;
            }
        };
        userService.getUserBooksByUserNick(anyString);
        result = null;
        times = 1;
    }
};
ResultActions httpResult = this.mockMvc.perform(get("/education/getBookList.do?userNick=" + nick))
    .andDo(print()).andExpect(status().isOk());
MockHttpServletResponse response = httpResult.andReturn().getResponse();
String responseStr = response.getContentAsString();
// 若是存在多版本客戶端的狀況下,注意返回值向後兼容,此處須要多種格式驗證.
Assert.assertEquals(responseStr, resultCheck);
}

7.3 工具類

靜態工具類測試示例。
1. 靜態方法:

java @Test public void testMethod() { new StrictExpectations(CookieUtil) { { CookieUtil.isLogined(); result = 

java @Test public void testMethod() { new MockUp<CookieUtil>(){ @Mock public boolean isLogined(){ return true; 

8總結

單元測試永遠沒法證實代碼的正確性!!
一個跑失敗的測試可能代表代碼有錯誤,但一個跑成功的測試什麼也證實不了。
單元測試最有效的使用場合是在一個較低的層級驗證並文檔化需求,以及迴歸測試:開發或重構代碼,不會破壞已有功能的正確性。

以上內容就是本篇的所有內容以上內容但願對你有幫助,有被幫助到的朋友歡迎點贊,評論。若是對軟件測試、接口測試、自動化測試、面試經驗交流。感興趣能夠關注博主主頁,會有同行一塊兒技術交流哦。

相關文章
相關標籤/搜索