工做多年後我更瞭解了UT的重要性

對於有經驗的開發寫單元測試是很是有必要的,而且對本身的代碼質量以及編碼能力也是有提升的。單元測試能夠幫助減小bug泄露,經過運行單元測試能夠直接測試各個功能的正確性,bug能夠提早發現並解決,因爲能夠跟斷點,因此可以比較快的定位問題,比泄露到生產環境再定位要代價小不少。同時充足的UT是保證重構正確性的有效手段,有了足夠的UT防禦,才能放開手腳大膽重構已有代碼,工 做多年後更瞭解了UT,瞭解了UT的重要性。java

單元測試

在敏捷的開發理念中,覆蓋全面的自動化測試是添加新特性和重構的必要前提。單元測試在軟件開發過程當中的重要性不言而喻,特別是在測試驅動開發的開發模式愈來愈流行的前提下,單元測試更成爲了軟件開發過程當中不可或缺的部分。同時單元測試也是提升軟件質量,花費成本比較低的重要方法。數據庫

1.單元測試的時機和測試點

1.1單元測試的時機

  1. 在業務代碼前編寫單元測試採用測試驅動開發,這是咱們常用和推薦的。
  2. 在業務代碼過程當中進行單元測試,對重要的業務邏輯和複雜的業務邏輯進行添加測試。
  3. 在業務邏輯以後再編寫測試是咱們不建議的,除非對遺留代碼的修改,須要先進行測試用例的添加,保證咱們修改和重構後的代碼不會破壞以前的業務邏輯。

1.2單元測試的測試點

  1. 在邏輯複雜的代碼中添加測試。
  2. 在容易出錯的地方添加測試。
  3. 不易理解的代碼中添加測試,在之後看到測試就能夠很是清楚代碼要實現的邏輯。
  4. 在考慮後期需求變動相對較大的代碼中添加測試,這樣後期需求更變修改代碼以後就不用太擔憂寫的代碼對不對以及是否破壞了已有代碼邏輯。
  5. 外部接口處添加解耦代碼、同時增長單元測試。

2.代碼不可測試性的根源

  1. 代碼中調用到了底層平臺的接口或只有系統運行後才能得到的資源(數據庫鏈接、發送郵件,網絡通信,遠程服務, 文件系統等)但業務代碼與這些資源未解耦。這樣在測試代碼須要建立這個類的時候會去初始化這些資源時致使沒法測試。
  2. 在方法內部new一個與本次測試無關的對象。
  3. 代碼依賴層次很深,邏輯複雜,一次方法的每每要調用N次底層的接口,或者類的方法很是多。這樣的代碼咱們須要對類進行重構,儘可能保證類的單一職責:這個類在系統中的意圖應當是單一的,且修改它的緣由應該只有一個。
  4. 使用單例類和靜態方法,而且單例類和靜態方法使用到了咱們底層的接口或者其餘接口。

3.測試工具使用和測試方法介紹

在作單元測試的時候,咱們會發現咱們要測試的方法會引用不少外部依賴的對象,如調用平臺接口、鏈接數據庫、網絡通信、遠程服務、FTP、文件系統等等。 而咱們無法控制這些外部依賴的對象,爲了解決這個問題,咱們就須要用到Mock工具來模擬這些外部依賴的對象,來完成單元測試。
如今比較流行的Mock工具備JMock、EasyMock、Mockito、PowerMock。咱們使用的是Mockito和PowerMock。PowerMock彌補了其餘3個Mock工具不能mock靜態、final 、私有方法的缺點。
在下面的狀況下咱們可使用Mock對象來完成單元測試。緩存

  1. 實對象具備不可肯定的行爲,會產生不可預測的結果。 如:數據庫查詢能夠查出一條記錄、多條記錄、或者返回數據庫異常等結果。
  2. 真實對象很難被建立。如:平臺代碼,或者Web、JBoss容器等。
  3. 真實對象的某些行爲很難觸發。 如:代碼中須要處理的網絡異常、數據庫異常、消息發送異常等。
  4. 真實狀況令程序運行很慢。 在敏捷的實踐中咱們完成了CI,在開發提交代碼前須要執行整個項目的單元測試用例,只有測試經過才能夠提交代碼。這就要求咱們每一個單元測試用例須要儘量的短,整個項目的測試時間纔會短。當有的測試用例須要測試大數據量狀況下系統的預期時,就須要使用Mock對象。
    如咱們代碼中須要判斷只有當系統的緩存隊列大於40000時,咱們開始考慮丟棄非關鍵的消息,當超過48000時,須要只處理最重要的消息,當超過50000時須要丟棄所有消息。此時就須要對此緩存隊列進行Mock,根據調用返回不一樣的數據量給測試。
  5. 測試須要知道真實對象是如何被調用的。如:測試用例須要驗證是否發送了JMS,此時就能夠經過Mock對象是否被調用來測試。
  6. 真實對象實際不存在時。 如:當咱們與其餘模塊交互時,或者與新的接口打交道時,更有就是對方的代碼尚未開發完畢時,咱們能夠經過Mock來模擬接口的行爲,實現代碼邏輯的驗證和測試。

3.1 Mocktio簡單使用說明

mock能夠模擬各類各樣的對象,從而代替真正的對象作出但願的響應。服務器

一、模擬對象的建立網絡

List cache = mock(ArrayList.class);
System.out.println(cache.get(0));
//-> null 因爲沒有對mock對象給預期,因此返回都是null

二、模擬對象方法調用的返回值多線程

List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn("hello");
System.out.println(cache.get(0));
//-> hello

三、模擬對象方法屢次調用和屢次返回值併發

List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn("0").thenReturn("1").thenReturn("2");
System.out.println(cache.get(0));
System.out.println(cache.get(0));
System.out.println(cache.get(0));
System.out.println(cache.get(0));
//-> 0,1,2,2 若是實際調用的次數超過了預期的次數,則會一直返回最後一次的預期值。

四、模擬對象方法調用拋出異常app

List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn(new Exception("Exception"));
System.out.println(cache.get(0));

五、模擬對象方法在沒有返回值時也能夠拋異常框架

List cache = mock(ArrayList.class);
doThrow(new Exception("Exception")).when(cache).clear();

六、模擬方法調用時的參數匹配ide

AnyInt的使用,匹配任何int參數
List cache = mock(ArrayList.class);
when(cache.get(anyInt())).thenReturn("0");
System.out.println(cache.get(0));
System.out.println(cache.get(2));
//-> 0,0

七、模擬方法是否被調用和調用的次數,預期調用了一次

List cache = mock(ArrayList.class);
cache.add("steven");
verify(cache).add("steven");

預期調用了兩次入緩存,沒有調用清除緩存的方法

List cache = mock(ArrayList.class);
cache.add("steven");
cache.add("steven");
verify(cache,times(2)).add("steven");
verify(cache,never()).clear();

還能夠經過atLeast(int i)和atMost(int i)來替代times(int i)來驗證被調用的次數最小值和最大值。
【注意】
Mock對象默認狀況下,對於全部有返回值且沒有預期過的方法,Mocktio會返回相應的默認值。對於內置類型會返回默認值,如int會返回0,布爾值返回false。對於其餘type會返回null。mock對象會覆蓋整個被mock的對象,所以沒有預期的方法只能返回默認值。這個在初次使用Mock時須要注意,常常會發現測試結果不對,最後才發現本身未給相應的預期。

3.2 PowerMock簡單使用說明

PowerMock使用一個自定義類加載器和字節碼操做來模擬靜態方法,構造函數,final類和方法,私有方法,去除靜態初始化器等等。
PowerMock使用簡單,在類名前添加註解,在預期前調用PowerMock的mock靜態類方法,其餘的預期方法和Mockito相似。

@PrepareForTest(System.class)
@RunWith(PowerMockRunner.class)
public class Test {
@org.junit.Test
public void should_get_filed() {
    System.out.println(System.getProperty("myName"));
    PowerMockito.mockStatic(System.class);
    PowerMockito.when(System.getProperty("myName")).thenReturn("steven");
    System.out.println(System.getProperty("myName"));
    //->null steven
    }
}

3.3 Fake對象的使用

測試中須要模擬對象,除了經常使用的mock對象外,咱們還會常常用到Fake對象。Mock對象是預先計劃好的對象,帶有各類期待,他們組成了一個關於他們期待接受的調用的詳細說明。而Fake對象是有實際可工做的實現,可是一般有一些缺點致使不適合用於產品,咱們一般使用Fake對象在測試中來模擬真實的對象。
在測試中常常會發現咱們須要使用系統或者平臺給咱們提供的接口,在測試中咱們能夠新建立一個類去實現此接口,而後在根據具體狀況去實習此模擬類的相應方法。

如咱們建立了本身的FakeLog對象來模擬真實的日誌打印,這樣咱們能夠在測試類中使用FakeLog來代替代碼中真實使用的Log類,能夠經過FakeLog的方法和預期的結果比較來進行測試正確性的判斷。

Fake對象和mock對象還有一個實際中使用的區別,Fake對象咱們構造好後,之後全部的代碼都去調用此Fake對象就能夠了,不用每一個類每次都要給預期。從這個角度能夠看到當一個類的方法或者預期相對不變時,能夠採用Fake對象,當這個類的返回信息預期變化很是不可預期時,能夠採用MOCK對象。

3.4Mock服務的兩種方式

(1)直接注入:用於類之間的依賴層次較多的狀況,測試整個業務流程,粒度大。

ResourceServerService service = mock(ResourcePPUServerService.class);
new Processor().process(service );

(2)重寫protected方法返回mock對象:用於類直接依賴於該服務的狀況,測試行爲的細節,粒度小。

ResourceServerService service = mock(ResourceServerService .class);
generator = new EutranAnrDeletingItemGenerator() {
    @Override
    protected ResourceServerService getService() {
        return service;
    }
}

3.5測試異常

Throwable有兩個直接子類:Exception和Error

一、expcetd=SomeExecption.class

@Test(expected = AssertionError.class)
public void should_occur_assertion_error_when_emb_number_is_not_eutran_or_utran_anr_delete() throws Exception {
    EMBObject eMBObject = new EMBObject();
    new AnrDeleteProcessor().getAnrDeleteGenerator(EMBObject);
}

@Test(expected = NumberFormatException.class)
public void should_throw_number_format_exception_when_input_string_field_greater_255() {
    TransactionIDConvert.convertTransIDToLong(transactionError);
}

二、try-catch-fail只能用於Exception,Error不能用此種方式

try {
    method.invoke();
    fail();
} catch (Exception e) {
    assertTrue(e.getCause() instanceof RuntimeException);
}

3.6私有方法—採用反射來調用

@Test
public void should_throw_runtime_exception_when_check_eutran_trap_data_fail() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    when(eutranAnrAddItemGenerator.getSrvCelProcessor()).thenReturn(processor);
    when(processor.validateTrapData(any(AnrItem.class), any(AnrBean.class))).thenReturn(false);

    Method method = EutranAnrAddItemGenerator.class.getDeclaredMethod("check", AnrItem.class);
    method.setAccessible(true);
    try {
        method.invoke(eutranAnrAddItemGenerator, anrAddItem);
    } catch (Exception e) {
        assertTrue(e.getCause() instanceof RuntimeException);
    }
}

4.單元測試的格式

4.1測試類結構

public class ExampleTest {
    @BeforeClass
    public static void setUp() throws Exception {
        initGlobalParameter();
        registerServices();
    }

    @Before
    public void setUp() throws Exception {
        initGlobalParameter();
        registerServices();
    }

    @After
    public void tearDown() throws Exception {
        ServiceAccess.serviceMap.clear();
        clearCache();     }

    @AfterClass
    public static void tearDown() throws Exception {
        ServiceAccess.serviceMap.clear();
        clearCache();
    }

    @Test
    public void should_get_some_result1_when_give_some_condition1{
    }

    @Test
    public void should_get_some_result2_when_give_some_condition2{
    }
}

JUnit4是JUnit框架有史以來的最大改進,其主要目標即是利用Java5的Annotation特性簡化測試用例的編寫。先簡單解釋一下什麼是Annotation,這個單詞通常是翻譯成元數據。元數據是什麼?元數據就是描述數據的數據。也就是說,這個東西在Java裏面能夠用來和public、static等關鍵字同樣來修飾類名、方法名、變量名。修飾的做用描述這個數據是作什麼用的,差很少和public描述這個數據是公有的同樣。

  • @Before:每一個測試方法執行以前都要執行一次。
  • @After:before對應,每一個測試方法執行以後要執行一次。
  • @BeforeClass:在全部測試方法以前運行,只運行一次。通常在此類中申請昂貴的外部資源。父類中有@BeforeClass方法,在其子類運行以前也會運行。
  • @AfterClass:與BeforeClass對應,在全部測試結束後,釋放BeforeClass中申請的資源。 注意:@Before,@After,@BeforeClass,@AfterClass 標示的方法一個類中只能各有一個
  • @Test: 告訴JUnit,該方法要做爲一個測試用例來運行。

4.2測試代碼的位置

在Java中一個包能夠橫跨兩個不一樣的目錄,因此咱們的測試代碼和產品代碼放在同一目錄中,這樣維護起來更方便,測試代碼和產品代碼在同一個包中,這樣也減小了沒必要要的包引發,同時在測試類中使用繼承更加的方便。

4.3測試用例格式3段式

一個測試用例主體內容通常採用三段式:given-when-then

  • Given:構造測試條件;
  • When:執行待測試的方法;
  • Then:判斷測試結果是否符合指望。
    例如:
@Test
public void should_get_correct_result_when_add_two_numbers() {
    int a = 1;
    int b = 2;

    int c = MyMath.add(a, b);

    assertEquals(3, c);
}

4.4類名的命名方式

測試類的名稱以Test結尾。從目標類的類名衍生出其單元測試類的類名。類名前加上Test後綴。
Fake(僞類)放在測試包中,使用前綴Fake。

4.5方法名的定義方式

should …do something…when…under some conditions…

例如:

should_NOT_delete_A_when_exists_B_related_with_A
should_throw_exception_when_the_parameter_is_illegal

4.6業務代碼中爲測試提供的方法的註解

在業務代碼中爲了測試而單獨提供的保護方法或者其餘方法,咱們經過@ForTest來標註。FofTest類以下:

@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
public @interface ForTest {
    String description() default "";
}

5.代碼中涉及外部接口時,如何來編寫單元測試

咱們的代碼涉及的模塊很是衆多,常常須要相互協做來完成一個功能,在此過程當中常常須要使用到外部的接口、同時也爲別的模塊提供服務。

5.1數據庫

數據庫的單元測試,因爲測試沒法進行數據庫的鏈接,故咱們經過提取通用接口(DBManagerInterface)和FakeDBManager來實現數據庫解耦。FakeDBManager能夠對真實的數據庫進行模擬,也就是咱們經過Fake一個簡單的內存數據庫來模擬實際真實的數據庫。
DBManager是咱們的真實鏈接數據庫的業務類。咱們在測試時,是能夠經過注入的方式用FakeDBManager來替換DBManager。

5.2平臺接口

5.2.1 平臺接口的Mock

平臺中的MinosMmlPPUServerService、ResourcePPUServerService等服務接口,均可以經過mock來進行測試。須要注意的是在業務代碼中須要進行相應的解耦,能夠經過SET方法或者構造器來注入平臺的服務類。

public class ICMEMBMessageListenerTest {
    private MinosMmlPPUServerService  minosMmlPPUServerService = mock(MinosMmlPPUServerService.class);

@Before
public void setUp() throws Exception {
    registerServices();
    icmembMessageListener = new ICMEMBMessageListener(){
    };
    when(minosMmlPPUServerService.getIp()).thenReturn("127.0.0.1");
    when(minosMmlPPUServerService.getPort()).thenReturn("80");
    when(minosMmlPPUServerService.getEmbPort()).thenReturn("8080");
}

此處須要注意若是用到靜態變量全局惟一的,須要在使用後在 tearDown中進行清除。

5.3 文件接口的測試

咱們的業務中也會出現與外部文件進行讀寫的代碼。按照單元測試書寫的原則,單元測試應該是獨立的,不依賴於外部任何文件或者資源的。好的單元測試是運行速度快,可以幫助咱們定位問題。因此咱們普通涉及到外部文件的代碼,都須要經過mock來預期其中的信息,如MOCK(I18n)文件或者properties、xml文件中的數據。
對於一些重要的文件,考慮到資源消耗不大的狀況下,咱們也會去爲這些文件添加單元測試。須要訪問真實的文件,咱們第一步就須要去獲取資源文件的具體位置。經過下面的FileService的getFileWorkDirectory咱們能夠獲取單元測試運行時的根目錄。

public class FileService {
public static String getFileWorkDirectory() {
    return new StringBuilder(getFileCodeRootDirectory()).append("test").toString();
}

public static String getFileCodeRootDirectory() {
    String userDir = System.getProperty("user.dir");
    userDir = userDir.substring(0, userDir.indexOf(File.separator + "CODE" + File.separator));
    StringBuilder workFilePath = new StringBuilder(userDir);
    workFilePath.append(File.separator).append("CODE").append(File.separator);
    return workFilePath.toString();
}
}

咱們在單元測試中能夠經過傳入具體的文件名稱,能夠在測試代碼中訪問真實的文件。
這種方法能夠適用I18n文件,xml文件, properties文件。
咱們在對I18n文件進行測試時,也能夠經過Fake對象根據具體的語言來進行國際化信息的測試。具體FakeI18nWrapper的代碼在第7章中給出能夠參考。

@Before
public void setUp() throws Exception {
    String i18nFilePath = FileService.getFileWorkDirectory() + "\\conf\\i18n.xml";
    I18N i18N = new FakeI18nWrapper(new File(i18nFilePath), I18nLanguageType.en_US);
    I18nAnrOsf.setTestingI18NInstance(i18N);
}

6.單元測試中涉及多線程、單例類、靜態類的處理

6.1多線程測試

經過單元測試,能較早地發現 bug 而且能比不進行單元測試更容易地修復bug。可是普通的單元測試方法(即便當完全地進行了測試時)在查找並行 bug 方面不是頗有效。這就是爲何在實驗室測試沒有問題,但在外場常常出現各類莫名其妙的問題。
爲何單元測試常常遺漏並行 bug?一般的說法是並行程序和Bug的問題在於它們的不肯定性。可是對於單元測試目的而言,在於並行程序是很是 肯定的。因此咱們單元測試須要對關鍵的邏輯、涉及到併發的場景進行多線程測試。
多線程的不肯定性和單元測試的肯定的預期確實是有點矛盾,這就須要精心的設計單元測試中的多線程用例。
Junit自己是不支持普通的多線程測試的,這是由於Junit的底層實現上是用System.exit退出用例執行的。JVM都終止了,在測試線程啓動的其餘線程天然也沒法執行。因此要想編寫多線程Junit測試用例,就必須讓主線程等待全部子線程執行完成後再退出。咱們通常的方法是在主測試線程中增長sleep方法,這種方法優勢是簡單,但缺點是不一樣機器的配置不同,致使等待時間沒法肯定。更爲高效的多線程單元測試可使用JAVA的CountDownLatch和第三方組件GroboUtils來實現。
下面經過一個簡單的例子來講明下多線程的單元測試。
測試的業務代碼以下,功能是惟一事務號的生成器。

class UniqueNoGenerator {
    private static int generateCount = 0;

    public static synchronized int getUniqueSerialNo() {
        return generateCount++;
    }
}

6.1.1 Sleep

private static Set<Integer> results = new HashSet<>();

@Test
public void should_get_unique_no() throws InterruptedException {
    Thread[] threads = new Thread[100];
    for (int i = 0; i < threads.length; i++) {
    threads[i] = generateThread();
    }
    //啓動線程
    Arrays.stream(threads).forEach(Thread::start);
    Thread.sleep(100L);
    
    assertEquals(results.size(), 100);
 }

private Thread generateThread() {
    return new Thread(() -> {
        int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
        results.add(uniqueSerialNo);
    });
}

經過Sleep來等待測試線程中的全部線程執行完畢後,再進行條件的預期。問題就是用戶沒法準確的預期業務代碼線程執行的時間,不一樣的環境等待的時間也是不等的。因爲須要添加延時,同時也違背了咱們單元測試執行時間須要儘可能短的原則。

6.1.2 ThreadGroup

private static Set<Integer> results = new HashSet<>();
private ThreadGroup threadGroup = new ThreadGroup("test");

@Test
public void should_get_unique_no() throws InterruptedException {
    Thread[] threads = new Thread[100];
    for (int i = 0; i < threads.length; i++) {
    threads[i] = generateThread();
 }
    //啓動線程
    Arrays.stream(threads).forEach(Thread::start);
    while (threadGroup.activeCount() != 0) {
    Thread.sleep(1);
    }
    assertEquals(results.size(), 100);
    }
    
    private Thread generateThread() {
    return new Thread(threadGroup, () -> {
    int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
    results.add(uniqueSerialNo);
    });
}

這個是經過ThreadGroup來實現多線程測試的,能夠把須要測試的類放入一個線程組,同時去判斷線程組中是否還有未結束的線程。測試中須要注意把新建的線程加入到線程組中。

6.1.3 CountDownLatch

private static Set<Integer> results = new HashSet<>();
private CountDownLatch countDownLatch = new CountDownLatch(100);

@Test
public void should_get_unique_no() throws InterruptedException {
    Thread[] threads = new Thread[100];
    for (int i = 0; i < threads.length; i++) {
        threads[i] = generateThread();
    }
    //啓動線程
    Arrays.stream(threads).forEach(Thread::start);
    countDownLatch.await();

    assertEquals(results.size(), 100);
}

private Thread generateThread() {
    return new Thread(() -> {
        int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
        results.add(uniqueSerialNo);
        countDownLatch.countDown();
    });
}

經過JAVA的CountDownLatch能夠很方便的來判斷,測試中的線程是否已經執行完畢。CountDownLatch是一個同步輔助類,在完成一組正在其餘線程中執行的操做以前,它容許一個或多個線程一直等待,咱們這裏是讓測試主線程等待。countDown方法是當前線程調用此方法,則計數減一。awaint方法,調用此方法會一直阻塞當前線程,直到計時器的值爲0。

6.2單例類測試

單例模式要點:

  1. 單例類在一個容器中只有一個實例。
  2. 單例類使用靜態方法本身提供向客戶端提供實例,本身擁有本身的引用。
  3. 必須向整個容器提供本身的實例。
    單例類的實現方式有多種方式,如懶漢式單例、餓漢式單例、登記式單例等。咱們這裏採用內部類的形式來構造單例類,實現的優勢是此種方式不須要給類或者方法添加鎖,惟一實例的生成是由JAVA的內部類生成機制保證。
    下面的例子構造了一個單例類,同時這個單例類咱們提供了一個獲取遠程Cpu信息的方法。再構造一個使用類ResourceManager.java來模擬調用此單例類,同時看下咱們測試ResourceManager.java過程當中遇到的問題。
    單例類DBManagerTools.java:
public class DbManager {
         private DbManager() {
         }
         
         public static DbManager getInstance() {
         return DbManagerHolder.instance;
         }
         
         private static class DbManagerHolder {
         private static DbManager instance = new DbManager();
         }
         
         public String getRemoteCpuInfo(){
         FtpClient ftpClient = new FtpClient("127.0.0.1","22");
         return ftpClient.getCpuInfo();
         }
     }

調用類 ResourceManager.java:

public class ResourceManager {
    public String getBaseInfo() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("IP=").append("127.0.0.1").append(";CPU=").append(DbManager.getInstance().getRemoteCpuInfo());
        return buffer.toString();
    }
}

測試類 
@Test
public void should_get_cpu_info() {
    String expected = "IP=127.0.0.1;CPU=Intel";
    ResourceManager resourceManager = new ResourceManager();

    String baseInfo = resourceManager.getBaseInfo();

    assertThat(baseInfo, is(expected));
}

從上面的描述能夠看到,因爲業務代碼強關聯了一個單例類,同時這個單例類會去經過網絡獲取遠程機器的信息。這樣咱們的單元測試在運行中就會去鏈接網絡中的服務器致使測試失敗。在業務類中相似這種涉及到單例類的調用常常用到。
這種狀況下咱們須要修改下業務代碼使代碼可測。
第一種方法:提取方法並在測試類中複寫。

public class ResourceManager {
    public String getBaseInfo() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("IP=").append("127.0.0.1").append("CPU=").append(getRemoteCpuInfo());
        return buffer.toString();
    }

    @ForTest
    protected String getRemoteCpuInfo() {
        return DbManager.getInstance().getRemoteCpuInfo();
    }
}

@Test
public void should_get_cpu_info() {
    String expected = "IP=127.0.0.1;CPU=Intel";
    ResourceManager resourceManager = new ResourceManager(){
        @Override
        protected String getRemoteCpuInfo() {
            return "Intel";
        }
    };

    String baseInfo = resourceManager.getBaseInfo();

    assertThat(baseInfo, is(expected));
}

第二種方法:提取單例類中的方法爲接口,而後在業務代碼中經過set方法或者構造器注入到業務代碼中。

public class DbManager implements ResourceService{
    private DbManager() {
    }

    public static DbManager getInstance() {
        return DbManagerHolder.instance;
    }

    private static class DbManagerHolder {
        private static DbManager instance = new DbManager();
    }

    @Override
    public String getRemoteCpuInfo(){
        FtpClient ftpClient = new FtpClient("127.0.0.1","22");
        return ftpClient.getCpuInfo();
    }

public interface ResourceService {
    String getRemoteCpuInfo();
}

public class ResourceManager {
    private ResourceService resourceService = DbManager.getInstance();

    public String getBaseInfo() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("IP=").append("127.0.0.1").append("CPU=").append(resourceService.getRemoteCpuInfo());
        return buffer.toString();
    }

    public void setResourceService(ResourceService resourceService) {
        this.resourceService = resourceService;
    }
}

@Test
public void should_get_cpu_info() {
    String expected = "IP=127.0.0.1;CPU=Intel";
    ResourceManager resourceManager = new ResourceManager();
    DbManager mockDbManager = mock(DbManager.class);
    resourceManager.setResourceService(mockDbManager);
    when(mockDbManager.getRemoteCpuInfo()).thenReturn("Intel");
    
    String baseInfo = resourceManager.getBaseInfo();

    assertThat(baseInfo, is(expected));
}

經過上面的方法能夠方便的解開業務代碼對單例的強依賴,有時候咱們發現咱們的業務代碼是靜態類,這個時候你會發下第一種方法是解決不了問題的,只能經過第2中方法來實現。
經過上面的代碼能夠看到咱們應該儘可能的少用單例,在必須使用單例時能夠設計接口來進行業務與單例類的解耦。

6.3靜態類測試

靜態類與單例類相似,也能夠經過提取方法後經過復現方法來解耦,一樣也能夠經過服務注入的方式來實現。也可使用PowerMock來預期方法的返回。
實際應用中若是單例類不須要維護任何狀態,僅僅提供全局訪問的方法,這種狀況考慮可使用靜態類,靜態方法比單例更快,由於靜態的綁定是在編譯期就進行的。
同時須要注意的是不建議在靜態類中維護狀態信息,特別是在併發環境中,若無適當的同步措施而修改多線程併發時,會致使壞的競態條件。
單例與靜態主要的優勢是單例類比靜態類更具備面向對象的能力,使用單例,能夠經過繼承和多態擴展基類,實現接口和更有能力提供不一樣的實現。
在咱們開發過程當中考慮到單元測試,仍是須要謹慎的使用靜態類和單例類。

7.代碼可測性的解耦方法

在使用一些解依賴技術時,咱們經常會感受到許多解依賴技術都破壞了原有的封裝性。但考慮到代碼的可測性和質量,犧牲一些封裝性也是能夠的,封裝自己也並非最終目的,而是幫助理解代碼的。下面在介紹下經常使用的解依賴方法。這些解依賴方法的思想都是通用的,採用控制反轉和依賴注入的方式來進行。

7.1儘可能減小業務代碼與平臺代碼之間的耦合

軟件開發中調用平臺服務查詢資源屬性的典型代碼:

public class DataProceeor{
    private static final SomePlatFormService service = ServerService.lookup(SomePlatFormService.ID);
    public static CompensateData getAttributes(String name){
        service.queryCompensate(name);
    }
}

這種代碼在實現上沒有問題,可是沒法進行單元測試(不啓動軟件)。由於此類加載時須要獲取平臺查詢資源相關的服務,業務代碼與平臺代碼存在強耦合性。
在不破壞原有功能的基礎上對這段代碼作以下改造:

一、引入實例變量和構造器

public class DataProceeor{
    private static final SomePlatformService service = ServerService.lookup(SomePlatformService.ID);
    private SomePlatformService _service;

    public DataProceeor(SomePlatformService service) {
        _service = service;
    }

    public DataProceeor() {
        _service = ServerService.lookup(SomePlatformService.ID);;
    }

    public CompensateData getAttributes(String name){
        service.queryCompensate(name);
    }
}

二、增長新方法

public CompensateData getSomeAttributes(String name){
    _service.queryCompensate(name);
}

三、查找代碼中全部用到方法getAttributes的地方,所有替換成getSomeAttributes。

四、完成第3步後,刪除已經無用的變量和方法。

五、重命名引入的變量和方法,使其符合命名規範。

public class DataProceeor{
    private SomePlatformService service;
    public DataProceeor(SomePlatformService service){
        this.service = service;
    }

    public DataProceeor() {
        service = ServerService.lookup(SomePlatformService.ID);;
    }

    public static CompensateData getAttributes(String name){
        service.queryCompensate(name);
    }
}

六、增長對新方法的測試用例

public class DataProcessorTest {
    private DataProceeor dataProceeor;
    private SomePlateService somePlateService;
    private Map<String, String> attributes;

    @Before
    public void setUp() throws Exception {
        attributes.put("pci", "1");
    }

    @Test
    public void should_get_attributes() {
        somePlateService = mock(SomePlateService.class);
        when(somePlateService.queryAttribue()).thenReturn(attributes);

        dataProceeor = new DataProceeor();

        CompensateData compensateData = dataProceeor.getAttributes("pci");
        assertThat(compensateData.value(), is("1"));
        assertThat(compensateData.value(), is("2"));
    }
}

運行該測試用例,發現最後一句斷言沒有經過:
修改最後一句斷言爲:assertThat(attributeValue+"", not("2"));
再次運行測試,測試用例經過。

7.2 擴展平臺的部分類,實現測試的目的

模式1中的例子查詢資源屬性時沒有設置過濾條件,事實上大多數處理都是依賴其餘處理類:

public class NotificationDispatcher {
    private static Logger logger = LoggerFactory.getLogger(NotificationDispatcher.class);

    public void processMessage(String notificationMsg) {
        NotificationMsg notification = new Gson().fromJson(notificationMsg, NotificationMsg.class);
        Map<String, String> sctpInfo;
        try {
            sctpInfo = new NotificationParser().parse(notification.getMessage());
            logger.info("Parse notification xml success: " + sctpInfo);
            NotificationProcessor processor = new NotificationProcessorFactory().getProcessor(sctpInfo.get(CONFIGURATION_TYPE));
            processor.process(sctpInfo);
        } catch (Exception e) {
            logger.warn(String.format("Deal notification failed. Exception: %s", e.getMessage()), e);
        }
    }
}

在本例中,查詢MOI的Filter是在getCellMoi方法內部構造出來的,咱們能夠嘗試給getCellMoi方法編寫測試用例:
測試用例沒有經過,問題出在哪裏呢?
Debug代碼發現,在getCellMoi方法內部構造出來的Filter和咱們在測試代碼中構造的Filter並非同一個對象。很天然地想到爲Filter類編寫子類,並覆蓋其equals方法。
用自定義的Filter代替平臺的Filter:

public String getCellMoi(String cellName){
    Filter filter = new SelfFilter(cellName);
    return getAttributers(filter,"moi");
}

修改後測試用例運行經過。

7.3 巧用protedted方法實現測試的注入

在模式2中,因爲Filter是在getCellMoi內部構造的,而且沒有euqals方法,致使沒法測試。還能夠用別的方法對其進行改造。代碼示例以下:
1.提取protected方法buildFilter()

public void processMessage(String notificationMsg) {
    UmeNotificationMsg umeNotificationMsg = new Gson().fromJson(notificationMsg, UmeNotificationMsg.class);
    Map<String, String> sctpInfo;
    try {
        sctpInfo = new NotificationParser().parse(umeNotificationMsg.getMessage());
        logger.info("Parse notification xml success: " + sctpInfo);
        NotificationProcessor processor = getNotificationProcessorFactory().getProcessor(sctpInfo.get(CONFIGURATION_TYPE));

        processor.process(sctpInfo);
    } catch (Exception e) {
        logger.warn(String.format("Deal notification failed. Exception: %s", e.getMessage()), e);
    }
}

@ForTest
protected NotificationProcessorFactory getNotificationProcessorFactory() {
        return new NotificationProcessorFactory();
}

2.在測試代碼中重寫getNotificationProcessorFactory方法

@Before
public void setUp() throws Exception {
    NotificationProcessorFactory notificationProcessorFactory = mock(NotificationProcessorFactory.class);
    notificationDispatcher = new NotificationDispatcher(){
        @Override
        protected NotificationProcessorFactory getNotificationProcessorFactory() {
            return notificationProcessorFactory;
        }
    };
}

運行測試,能夠經過。

八、總結

UT是開發人員的利器,是開發的前置保護傘,也是寫出健壯代碼的有力保證,總之一句話不會寫UT的開發不是好廚子

相關文章
相關標籤/搜索