8點(diǎn)了解Java服務(wù)端單元測試

一. 前言

單元測試并不只是為了驗(yàn)證你當(dāng)前所寫的代碼是否存在問題,更為重要的是它可以很大程度的保障日后因業(yè)務(wù)變更、修復(fù)Bug或重構(gòu)等引起的代碼變更而導(dǎo)致(或新增)的風(fēng)險。

同時將單元測試提前到編寫正式代碼進(jìn)行(測試驅(qū)動開發(fā)),可以很好的提高對代碼結(jié)構(gòu)的設(shè)計。通過優(yōu)先編寫測試用例,可以很好的從用戶角度來對功能的分解、使用過程和接口等進(jìn)行設(shè)計,從而提高代碼結(jié)構(gòu)的高內(nèi)聚、低耦合特性。使得對日后的需求變更或代碼重構(gòu)等更加高效、簡潔。

因此編寫單元測試對產(chǎn)品開發(fā)和維護(hù)、技術(shù)提升和積累具有重大意義!

二. 第一個單元測試

首先寫一個單元測試,這樣有助于對后面內(nèi)容的理解與實(shí)踐。

2.1 開發(fā)環(huán)境

IntelliJ IDEA
IntelliJ IDEA默認(rèn)自帶并啟用TestNG和覆蓋率插件:

  • TestNG

在設(shè)置窗口查看TestNG插件是否安裝與啟用:


  • 覆蓋率

同樣,查看覆蓋率插件可以搜索“Coverage”。IntelliJ IDEA的覆蓋率統(tǒng)計工具有三種,JaCoCo、Emma和IntelliJ IDEA自帶。


  • 變異測試

同樣,查看并安裝變異測試插件可以搜索“PIT mutation testing”。


Eclipse
Eclipse需要自行安裝單元測試相關(guān)插件:

  • TestNG

執(zhí)行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 創(chuàng)建單元測試

下面介紹通過IDE自動創(chuàng)建單元測試的方法(也可手動完成):
IntelliJ IDEA

Eclipse:

2.在彈出的窗口中搜索“Test”,選擇“TestNG class”后點(diǎn)擊“Next”按鈕:

3.在窗口中選擇要創(chuàng)建的測試方法后點(diǎn)擊“Next”按鈕:

4.根據(jù)自己的情況設(shè)置包名、類名和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 {

    /**
     * 圖書持久化類,遠(yuǎn)程接口
     */
    @Injectable
    private BookDAO bookDAO;

    /**
     * 用戶服務(wù),遠(yuǎn)程接口
     */
    @Injectable
    private UserService userService;

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

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

2.4 運(yùn)行單元測試

IntelliJ IDEA

Eclipse

注:也可點(diǎn)擊工具欄選項(xiàng)運(yùn)行,從左至右依次是:覆蓋率、調(diào)試、運(yùn)行運(yùn)行。
2.點(diǎn)擊“運(yùn)行”:
左側(cè)框:單元測試運(yùn)行結(jié)果
底側(cè)框:單元測試打印輸出的內(nèi)容

Maven

  • 執(zhí)行目錄下所有單元測試,進(jìn)入工程目錄后執(zhí)行:mvn test
  • 執(zhí)行具體的單元測試類,多個測試類可用逗號分開:mvn test -Dtest=Test1,Test2
  • 執(zhí)行具體的單元測試類的方法:mvn test -Dtest=Test1#testMethod
  • 執(zhí)行某個包下的單元測試:mvn test -Dtest=com/alibaba/biz/*
  • 執(zhí)行ANT風(fēng)格路徑表達(dá)式下的單元測試:mvn test -Dtest=/Test或mvn test -Dtest=*/???Test
  • 忽略單元測試:mvn -Dmaven.test.skip=true

2.5 單元測試覆蓋

IntelliJ IDEA

Eclipse

2.輸出報告
運(yùn)行過程以及結(jié)果輸出的窗口中有一行“JMockit: Coverage report written to”,是EclEmma創(chuàng)建的覆蓋率報告文件目錄:

覆蓋率報告

2.6 變異測試

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

3. 輸出報告
運(yùn)行過程以及結(jié)果輸出的窗口中最后一行“Open report in browser”即為插件創(chuàng)建的報告連接。
點(diǎn)擊即可打開報告:

Eclipse

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

為今后更好的開展與落實(shí)單元測試,請繼續(xù)閱讀下面內(nèi)容。

3 單元測試框架

3.1 TestNG

Junit4TestNGJava非常流行的單元測試框架。因TestNG更加簡潔、靈活和功能豐富,所以我們選用TestNG
下面通過與Junit4的比較來了解一下TestNG的特性:

注解支持

Junit4TestNG的注解對比:

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

// TODO 測試 測試方法 測試套件 測試組 的區(qū)別
Junit4中,@BeforeClass@AfterClass只能用于靜態(tài)方法。TestNG無此約束。

異常測試

異常測試是指在單元測試中應(yīng)該要拋出什么異常是合理的。

  • 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");
}

時間測試

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

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

套件測試

套件測試是指把多個單元測試組合成一個模塊,然后統(tǒng)一運(yùn)行。

  • JUnit4

@RunWith@Suite注解被用于執(zhí)行套件測試。下面的代碼是所展示的是在“JunitTest5”被執(zhí)行之后需要“JunitTest1”和“JunitTest2”也一起執(zhí)行。所有的聲明需要在類內(nèi)部完成。
java

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

是使用XML配置文件來執(zhí)行套件測試。下面的配置將“TestNGTest1”和“TestNGTest2”一起執(zhí)行。

<!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的另一種方式使用了組的概念,每個測試方法都可以根據(jù)功能特性分配到一個組里面。例如:

@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文件配置了一個執(zhí)行組為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>

分組使集成測試更加強(qiáng)大。例如,我們可以只是執(zhí)行所有測試中的組名為DatabaseFuntion的測試。

參數(shù)化測試

參數(shù)化測試是指給單元測試傳多種參數(shù)值,驗(yàn)證接口對多種不同參數(shù)的處理是否正確。

  • JUnit4

@RunWith@Parameter注解用于為單元測試提供參數(shù)值,@Parameters必須返回List,參數(shù)將會被作為參數(shù)傳給類的構(gòu)造函數(shù)。

@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);
}
}

它的使用很不方便:一個方法的參數(shù)化測試必須定義一個測試類。測試參數(shù)通過一個注解為@Parameters且返回值為List參數(shù)值列表的靜態(tài)方法。然后將方法返回值成員通過類的構(gòu)造函數(shù)初始化為類的成員。最后再將類的成員做為參數(shù)去測試被測試方法。

  • TestNG

使用XML文件或@DataProvider注解兩種方式為測試提供參數(shù)。

XML文件配置參數(shù)化測試
方法上添加@Parameters注解,參數(shù)數(shù)據(jù)由TestNG的XML配置文件提供。這樣做之后,我們可以使用不同的數(shù)據(jù)集甚至是不同的結(jié)果集來重用一個測試用例。另外,甚至是最終用戶,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注解參數(shù)化測試
使用XML文件初始化數(shù)據(jù)雖然方便,但僅支持基礎(chǔ)數(shù)據(jù)類型。如需復(fù)雜的類型可使用@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作為對象的參數(shù)
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的參數(shù)化測試使用起來非常方便,它可以在一個測試類中添加多個方法的參數(shù)化測試(JUnit4一個方法就需要一個類)。

依賴測試

依賴測試是指測試的方法是有依賴的,在執(zhí)行的測試之前需要執(zhí)行的另一測試。如果依賴的測試出現(xiàn)錯誤,所有的子測試都被忽略,且不會被標(biāo)記為失敗。

  • JUnit4

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

  • TestNG

它使用dependOnMethods來實(shí)現(xiàn)了依賴測試的功能,如下:

@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()成功執(zhí)行,那么method2()也將被執(zhí)行,否則method2()將會被忽略。

性能測試

TestNG支持通過多個線程并發(fā)調(diào)用一個測試接口來實(shí)現(xiàn)性能測試。JUnit4不支持,若要進(jìn)行性能測試需手動添加并發(fā)代碼。

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

并行測試

TestNG支持通過多個線程并發(fā)調(diào)用多個測試接口執(zhí)行測試,相對于傳統(tǒng)的單線程執(zhí)行測試的方式,可以很大程度減少測試運(yùn)行時間。

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>

討論總結(jié)

通過上面的對比,建議使用TestNG作為Java項(xiàng)目的單元測試框架,因?yàn)門estNG在參數(shù)化測試、依賴測試以、套件測試(組)及并發(fā)測試方面功能更加簡潔、強(qiáng)大。另外,TestNG也涵蓋了JUnit4的全部功能。

3.2 JMockit

Mock的使用場景:

比如Mock以下場景:

  1. 外部依賴的應(yīng)用的調(diào)用,比如WebService等服務(wù)依賴。
  2. DAO層(訪問MySQL、Oracle、Emcache等底層存儲)的調(diào)用等。
  3. 系統(tǒng)間異步交互通知消息。
  4. methodA里面調(diào)用到的methodB。
  5. 一些應(yīng)用里面自己的Class(abstract,final,static)、Interface、Annotation、Enum和Native等。

Mock工具的原理:

Mock工具工作的原理大都如下:

  1. Record階段:錄制期望。也可以理解為數(shù)據(jù)準(zhǔn)備階段。創(chuàng)建依賴的Class或Interface或Method,模擬返回的數(shù)據(jù)、耗時及調(diào)用的次數(shù)等。
  2. Replay階段:通過調(diào)用被測代碼,執(zhí)行測試。期間會Invoke到第一階段Record的Mock對象或方法。
  3. Verify階段:驗(yàn)證??梢则?yàn)證調(diào)用返回是否正確,及Mock的方法調(diào)用次數(shù),順序等。

當(dāng)前的一些Mock工具的比較:

歷史曾經(jīng)或當(dāng)前比較流行的Mock工具有EasyMock、jMock、Mockito、Unitils Mock、PowerMockJMockit等工具。
從這里可以看到,JMockit的的功能最全面、強(qiáng)大!所以我們單元測試中的Mock工具也選擇了JMockit。同時在開發(fā)的過程中,JMockit的“Auto-injection of mocks”及“Special fields for “any” argument matching”及各種有用的Annotation使單元測試的開發(fā)更簡潔和高效。

JMockit的簡介:

JMockit是用以幫助開發(fā)人員編寫單元測試的Mock工具。它基于java.lang.instrument包開發(fā),并使用ASM庫來修改Java的Bytecode。正因此兩點(diǎn),它可以實(shí)現(xiàn)無所不能的Mock。

JMockit可以Mock的種類包含了:

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

JMockit有兩種Mock的方式:

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

通俗點(diǎn)講,Behavior-oriented是基于行為的Mock,對Mock目標(biāo)代碼的行為進(jìn)行模仿,像是黑盒測試。State-oriented是基于狀態(tài)的Mock,是站在目標(biāo)測試代碼內(nèi)部的。可以對傳入的參數(shù)進(jìn)行檢查、匹配,才返回某些結(jié)果,類似白盒。而State-oriented的new MockUp基本上可以Mock任何代碼或邏輯。

以下是JMockit的APIs和tools:

可以看到JMockit常用的Expectation、StrictExpectations和NonStrictExpectations期望錄制及注解@Tested、@Mocked,@NonStrict、@Injectable等簡潔的Mock代碼風(fēng)格。而且JMockit還自帶了Code Coverage的工具供本地單元測試時候邏輯覆蓋或代碼覆蓋率使用。

JMockit的使用:

以“第一個單元測試”代碼為例:

  • 測試對象

@Tested:JMockit會自動創(chuàng)建注解為“@Tested”的類對象,并將其做為被測試對象。 通過設(shè)置“availableDuringSetup=true”參數(shù),可以使得被測試對象在“setUp”方法執(zhí)行前被創(chuàng)建出來。

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

@Injectable:JMockit自動創(chuàng)建注解為“@Injectable”的類對象,并將其自動注入被測試對象。

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

相關(guān)的注解還有:// TODO 待補(bǔ)充

  • 錄制

Expectations:塊里的內(nèi)容是用來Mock方法,并指定方法的返回值、異常、調(diào)用次數(shù)和耗時。此塊中的方法是必須被執(zhí)行的,否則單元測試失敗。

/**
* 測試根據(jù)用戶的Nick查詢用戶的圖書列表方法
* 其中“getUserBooksByUserNick”方法最終需要通過UserId查詢DB,
* 所以在調(diào)用此方法之前需要先對UserService類的getUserIdByNick方法進(jìn)行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);
}

相關(guān)的類還有:

  • 結(jié)果驗(yàn)證

Assert:是最常見的斷言驗(yàn)證

Assert.assertNotNull(bookList);

Verifications:一種特殊的驗(yàn)證塊。比如:要驗(yàn)證一個被測試類中,調(diào)用的某個方法是否為指定的參數(shù)、調(diào)用次數(shù)。相比Expectations它放在單元測試的最后且沒有Mock功能。

注:以上列舉的注釋具體用法示例請查閱第7節(jié)內(nèi)容

4 單元測試內(nèi)容

在單元測試時,測試人員根據(jù)設(shè)計文檔和源碼,了解模塊的接口和邏輯結(jié)構(gòu)。主要采用白盒測試用例,輔之黑盒測試用例,使之對任何(合理和不合理)的輸入都要能鑒別和響應(yīng)。這就要求對程序所有的局部和全局的數(shù)據(jù)結(jié)構(gòu)、外部接口和程序代碼的關(guān)鍵部分進(jìn)行檢查。

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

4.1 模塊接口測試

在單元測試開始時,應(yīng)該對所有被測模塊的接口進(jìn)行測試。如果數(shù)據(jù)不能正常地輸入和輸出,那么其他的測試毫無意義。Myers在關(guān)于軟件測試的書中為接口測試提出了一個檢查表:

  • 模塊輸入?yún)?shù)的數(shù)目是否與模塊形式參數(shù)數(shù)目相同
  • 模塊各輸入的參數(shù)屬性與對應(yīng)的形參屬性是否一致
  • 模塊各輸入的參數(shù)類型與對應(yīng)的形參類型是否一致
  • 傳到被調(diào)用模塊的實(shí)參的數(shù)目是否與被調(diào)用模塊形參的數(shù)目相同
  • 傳到被調(diào)用模塊的實(shí)參的屬性是否與被調(diào)用模塊形參的屬性相同
  • 傳到被調(diào)用模塊的實(shí)參的類型是否與被調(diào)用模塊形參的類型相同
  • 引用內(nèi)部函數(shù)時,實(shí)參的次序和數(shù)目是否正確
  • 是否引用了與當(dāng)前入口無關(guān)的參數(shù)
  • 用于輸入的變量有沒有改變
  • 在經(jīng)過不同模塊時,全局變量的定義是否一致
  • 限制條件是否以形參的形式傳遞
  • 使用外部資源時,是否檢查可用性并及時釋放資源,如內(nèi)存、文件、硬盤、端口等

當(dāng)模塊通過外部設(shè)備進(jìn)行輸入/輸出操作時,必須擴(kuò)展接口測試,附加如下的測試項(xiàng)目:

  • 文件的屬性是否正確
  • Open與Close語句是否正確
  • 規(guī)定的格式是否與I/O語句相符
  • 緩沖區(qū)的大小與記錄的大小是否相配合
  • 在使用文件前,文件是否打開
  • 文件結(jié)束的條件是否會被執(zhí)行
  • I/O錯誤是否檢查并做了處理
  • 在輸出信息中是否有文字錯誤

4.2 局部數(shù)據(jù)結(jié)構(gòu)測試

模塊的局部數(shù)據(jù)結(jié)構(gòu)是最常見的錯誤來源,應(yīng)設(shè)計測試用例以檢查以下各種錯誤:

  • 不正確或不一致的數(shù)據(jù)類型說明
  • 使用尚未賦值或尚未初始化的變量
  • 錯誤的初始值或錯誤的默認(rèn)值
  • 變量名拼寫錯或書寫錯——使用了外部變量或函數(shù)
  • 不一致的數(shù)據(jù)類型
  • 全局?jǐn)?shù)據(jù)對模塊的影響
  • 數(shù)組越界
  • 非法指針

4.3 路徑測試

檢查由于計算、判定和控制流錯誤而導(dǎo)致的程序錯誤。由于在測試時不可能做到窮舉測試,所以在單元測試時要根據(jù)“白盒”測試和“黑盒”測試用例的設(shè)計方法設(shè)計測試用例,對模塊中重要的執(zhí)行路徑進(jìn)行測試。重要的執(zhí)行路徑是通常指那些處在具體實(shí)現(xiàn)的算法、控制、數(shù)據(jù)處理等重要位置的路徑,也可指較復(fù)雜而容易出錯的路徑。盡可能地對執(zhí)行路徑進(jìn)行測試非常重要,需要設(shè)計因錯誤的計算、比較或控制流而導(dǎo)致錯誤的測試用例。此外,對基本執(zhí)行路徑和循環(huán)進(jìn)行測試也可發(fā)現(xiàn)大量的路徑錯誤。

在路徑測試中,要檢查的錯誤有:死代碼、錯誤的計算優(yōu)先級、算法錯誤、混用不同類的操作、初始化不正確、精度錯誤——比較運(yùn)算錯誤、賦值錯誤、表達(dá)式的不正確符號——>、>=;=、==、!=和循環(huán)變量的使用錯誤——錯誤賦值以及其他錯誤等。

比較操作和控制流向緊密相關(guān),測試用例設(shè)計需要注意發(fā)現(xiàn)比較操作的錯誤:

  • 不同數(shù)據(jù)類型的比較(注意包裝類與基礎(chǔ)類型的比較)
  • 不正確的邏輯運(yùn)算符或優(yōu)先次序
  • 因浮點(diǎn)運(yùn)算精度問題而造成的兩值比較不等
  • 關(guān)系表達(dá)式中不正確的變量和比較符
  • “差1錯”,即不正常的或不存在的循環(huán)中的條件
  • 當(dāng)遇到發(fā)散的循環(huán)時無法跳出循環(huán)
  • 當(dāng)遇到發(fā)散的迭代時不能終止循環(huán)
  • 錯誤的修改循環(huán)變量

4.4 錯誤處理測試

錯誤處理路徑是指可能出現(xiàn)錯誤的路徑以及進(jìn)行錯誤處理的路徑。當(dāng)出現(xiàn)錯誤時會執(zhí)行錯誤處理代碼,或通知用戶處理,或停止執(zhí)行并使程序進(jìn)入一種安全等待狀態(tài)。測試人員應(yīng)意識到,每一行程序代碼都可能執(zhí)行到,不能自認(rèn)為錯誤發(fā)生的概率很小而不進(jìn)行測試。一般軟件錯誤處理測試應(yīng)考慮下面幾種可能的錯誤:

  • 出錯的描述是否難以理解,是否能夠?qū)﹀e誤定位
  • 顯示的錯誤與實(shí)際的錯誤是否相符
  • 對錯誤條件的處理正確與否
  • 在對錯誤進(jìn)行處理之前,錯誤條件是否已經(jīng)引起系統(tǒng)的干預(yù)等

在進(jìn)行錯誤處理測試時,要檢查如下內(nèi)容:

  • 在資源使用前后或其他模塊使用前后,程序是否進(jìn)行錯誤出現(xiàn)檢查
  • 出現(xiàn)錯誤后,是否可以進(jìn)行錯誤處理,如引發(fā)錯誤、通知用戶、進(jìn)行記錄
  • 在系統(tǒng)干預(yù)前,錯誤處理是否有效,報告和記錄的錯誤是否真實(shí)詳細(xì)

4.5 邊界測試

邊界測試是單元測試中最后的任務(wù)。代碼常常在邊界上出錯,比如:在代碼段中有一個n次循環(huán),當(dāng)?shù)竭_(dá)第n次循環(huán)時就可能會出錯;或者在一個有n個元素的數(shù)組中,訪問第n個元素時是很容易出錯的。因此,要特別注意數(shù)據(jù)流、控制流中剛好等于、大于或小于確定的比較值時可能會出現(xiàn)的錯誤。對這些地方需要仔細(xì)地認(rèn)真加以測試。

此外,如果對模塊性能有要求的話,還要專門對關(guān)鍵路徑進(jìn)行性能測試。以確定最壞情況下和平均意義下影響運(yùn)行時間的因素。下面是邊界測試的具體要檢查的內(nèi)容:

  • 普通合法數(shù)據(jù)是否正確處理
  • 普通非法數(shù)據(jù)是否正確處理
  • 邊界內(nèi)最接近邊界的(合法)數(shù)據(jù)是否正確處理
  • 邊界外最接近邊界的(非法)數(shù)據(jù)是否正確處理等
  • 在n次循環(huán)的第0次、第1次、第n次是否有錯誤
  • 運(yùn)算或判斷中取最大最小值時是否有錯誤
  • 數(shù)據(jù)流、控制流中剛好等于、大于、小于確定的比較值時是否出現(xiàn)錯誤

5 單元測試規(guī)范

5.1 命名規(guī)范

5.2 測試內(nèi)容

第4部分概括的列舉了需要測試的5大點(diǎn)內(nèi)容,此處為服務(wù)端代碼層至少要包含或覆蓋的測試內(nèi)容。
Service

  • 局部數(shù)據(jù)結(jié)構(gòu)測試
  • 路徑測試
  • 錯誤處理測試
  • 邊界測試

HTTP接口

  • 模擬接口測試
  • 局部數(shù)據(jù)結(jié)構(gòu)測試
  • 路徑測試
  • 錯誤處理測試
  • 邊界測試

HSF接口

  • 模擬接口測試
  • 局部數(shù)據(jù)結(jié)構(gòu)測試
  • 路徑測試
  • 錯誤處理測試
  • 邊界測試

工具類

  • 模擬接口測試
  • 局部數(shù)據(jù)結(jié)構(gòu)測試
  • 路徑測試
  • 錯誤處理測試
  • 邊界測試

5.3 覆蓋率

為了使單元測試能充分細(xì)致地展開,應(yīng)在實(shí)施單元測試中遵守下述要求:

  1. 語句覆蓋達(dá)到100%
    語句覆蓋指被測單元中每條可執(zhí)行語句都被測試用例所覆蓋。語句覆蓋是強(qiáng)度最低的覆蓋要求,要注重語句覆蓋的意義。比如,用一段從沒執(zhí)行過的程序控制航天飛機(jī)升上天空,然后使它精確入軌,這種行為的后果不敢想象。實(shí)際測試中,不一定能做到每條語句都被執(zhí)行到。第一,存在“死碼”,即由于代碼設(shè)計錯誤在任何情況下都不可能執(zhí)行到的代碼。第二,不是“死碼”,但是由于要求的輸入及條件非常難達(dá)到或單元測試的實(shí)現(xiàn)所限,使得代碼沒有得到執(zhí)行。因此,在可執(zhí)行語句未得到執(zhí)行時,要深入程序作做詳細(xì)的分析。如果是屬于以上兩種情況,則可以認(rèn)為完成了覆蓋。但是對于后者,也要盡量測試到。如果以上兩者都不是,則是因?yàn)闇y試用例設(shè)計不充分,需要再設(shè)計測試用例。

  2. 分支覆蓋達(dá)到100%
    分支覆蓋指分支語句取真值和取假值各一次。分支語句是程序控制流的重要處理語句,在不同流向上設(shè)計可以驗(yàn)證這些控制流向正確性的測試用命。分支覆蓋使這些分支產(chǎn)生的輸出都得到驗(yàn)證,提高測試的充分性。

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

  4. 單元的軟件特性覆蓋
    軟件的特性包括功能、性能、屬性、設(shè)計約束、狀態(tài)數(shù)目、分支的行數(shù)等。

  5. 對試用額定數(shù)據(jù)值、奇異數(shù)據(jù)值和邊界值的計算進(jìn)行檢驗(yàn)。用假想的數(shù)據(jù)類型和數(shù)據(jù)值運(yùn)行測試,排斥不規(guī)則的輸入。

單元測試通常是由編寫程序的人自己完成的,但是項(xiàng)目負(fù)責(zé)人應(yīng)當(dāng)關(guān)心測試的結(jié)果。所有的測試用例和測試結(jié)果都是模塊開發(fā)的重要資料,需妥善保存。

5.4 變異測試

測試覆蓋方法的確可以幫我們找到一些顯而易見的代碼冗余或者測試遺漏的問題。不過,實(shí)踐證明,這些傳統(tǒng)的方法只能非常有限的發(fā)現(xiàn)測試中的問題。很多代碼和測試的問題在覆蓋達(dá)到100%的情況下也無法發(fā)現(xiàn)。然而,“代碼變異測試”這種方法可以很好的彌補(bǔ)傳統(tǒng)方法的缺點(diǎn),產(chǎn)生更加有效的單元測試。

代碼變異測試是通過對代碼產(chǎn)生“變異”來幫助我們改進(jìn)單元測試的?!白儺悺敝傅氖切薷囊惶幋a來改變代碼行為(當(dāng)然保證語法的合理性)。簡單來說,代碼變異測試先試著對代碼產(chǎn)生這樣的變異,然后運(yùn)行單元測試,并檢查是否有測試是因?yàn)檫@個代碼變異而失敗。如果失敗,那么說明這個變異被“消滅”了,這是我們期望看到的結(jié)果。否則說明這個變異“存活”了下來,這種情況下我們就需要去研究一下“為什么”了。

總而言之,測試覆蓋這種方法是一種不錯的保障單元測試質(zhì)量的手段。代碼變異測試則比傳統(tǒng)的測試覆蓋方法可以更加有效的發(fā)現(xiàn)代碼和測試中潛在的問題,它可以使單元測試更加強(qiáng)壯。

6 CISE集成

省略

7 單元測試示例

7.1 Service

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

/**
* 測試根據(jù)用戶的Nick查詢用戶的圖書列表方法
* 其中“userService.getUserBooksByUserNick”方法最終需要通過UserId查詢DB,
* 所以在調(diào)用此方法之前需要先對UserService類的getUserIdByNick方法進(jìn)行Mock。
* 其中“bookDAO.getUserBooksByUserId”方法最終需要通過UserId查詢DB,
* 所以在調(diào)用此方法之前需要先對BookDAO類的getUserBooksByUserId方法進(jìn)行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; // 接口被調(diào)用的次數(shù)

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

2.錯誤(異常)處理:

/**
* 測試根據(jù)用戶的Nick查詢用戶的圖書列表方法,注意在@Test添加expectedExceptions參數(shù)
* 驗(yàn)證其中“userService.getUserBooksByUserNick”接口出現(xiàn)異常時,對異常的處理是否符合預(yù)期.
* 其中“bookDAO.getUserBooksByUserId”方法不會被調(diào)用到。
*/
@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; // 接口被調(diào)用的次數(shù)

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

3. Mock具體方法實(shí)現(xiàn):

/**
* 測試發(fā)送離線消息方法
* 消息隊(duì)列:當(dāng)離線消息超過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() {
{
    // 驗(yàn)證 addMsgByUserId 接口是否被調(diào)用了100次
    MsgDAO.addMsgByUserId(anyLong, withInstanceOf(MsgDO.class));
    times = testAddMsgCount;
    // 驗(yàn)證是否對消息內(nèi)容進(jìn)行相就次數(shù)的轉(zhuǎn)義
    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();
// 如果存在多版本客戶端的情況下,注意返回值向后兼容,此處需要多種格式驗(yàn)證.
Assert.assertEquals(responseStr, "{\"code\":1,\"msg\":\"success\",\"data\":\"\"}");
}
}

2. 參數(shù)化測試

@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();
// 如果存在多版本客戶端的情況下,注意返回值向后兼容,此處需要多種格式驗(yàn)證.
Assert.assertEquals(responseStr, resultCheck);
}

7.3 工具類

靜態(tài)工具類測試示例。
1. 靜態(tài)方法:

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總結(jié)

單元測試永遠(yuǎn)無法證明代碼的正確性?。?br> 一個跑失敗的測試可能表明代碼有錯誤,但一個跑成功的測試什么也證明不了。
單元測試最有效的使用場合是在一個較低的層級驗(yàn)證并文檔化需求,以及回歸測試:開發(fā)或重構(gòu)代碼,不會破壞已有功能的正確性。
以上內(nèi)容就是本篇的全部內(nèi)容以上內(nèi)容希望對你有幫助,有被幫助到的朋友歡迎點(diǎn)贊,評論。
如果對軟件測試、接口測試、自動化測試、面試經(jīng)驗(yàn)交流。感興趣可以關(guān)注小編,會有同行一起技術(shù)交流哦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容