Junit單元測試

前言:關於單元測試相信大家還是有所瞭解的,但是目前公司項目開發中真正應用單元測試卻不多,原因可能包括單元測試代碼量比實際業務代碼量大的多,開發效率低,或者說不會寫UT…

但是既然單元測試能被推廣出來,說明從某種角度講他的優端還是大於弊端的,尤其是在業務代碼複雜或者進行代碼重構的時候,在走向"真香定律"的道路上一去不復返,本篇博客將主要對於springboot整合Junit單元測試進行分析以及實戰介紹


1.單元測試情況分析

如果一個service實現依賴某個RPC service,那麼如果測試這個類的話需要準備哪些工作

數據準備

數據準備的時候不可能說跑到別人家的數據庫插幾條數據?或者跟PRC Service的Owner商量好,搭一個測試環境供給測試?那麼即使有測試環境,那如何實現各種case場景下,第三方Service很配合的返回數據給我們?這是個問題

執行方法

數據準備正常的情況下,假設service裏面調用了另一個RPC Service創建了很多數據,跑了無數次case,結果RPC Service對應的數據庫都是髒數據,如何清理?這是個問題

輸出驗證

數據準備和執行方法都正常的情況下,假設方法執行最終輸出是創建了一個訂單,訂單調用訂單Service接口,那麼如何驗證訂單是否成功創建了呢?或許可以調用訂單Service查詢訂單的接口來驗證,很明顯大多數情況下並沒有這麼perfect,this is a problem

通過上面分析,Local Integration Test(本地集成測試)是可行的,Remote Integration Test(遠程集成測試)基本不可行


2.Mock解決單測問題

對各個模塊的依賴可能是做單元測試過程中遇到最麻煩的問題,從網上的解決辦法來看,解決這類問題一般有兩個方法:

建立擋板環境,對於外部依賴的系統接口都建立擋板。雖然依賴的系統接口很多並且建立擋板環境會浪費很多資源,但對於單元測試而言這樣其實還是會依賴於擋板環境,每次切擋板和修改擋板數據也會比較麻煩 ,所以一般不建議採用擋板來進行單元測試

建立Mock類,對被測試的每個類中依賴的類都建立mock,並且對mock類用到的方法要寫樁和樁數據,但寫mock確實是單元測試過程中工作量最大的地方,一旦mock寫好以後,會發現被測類可以獨立於任何模塊,可以和一切解耦,當你寫單元測試過程中發現寫的mock很多的時候,這就說明這個類外部依賴太多


3.Springboot+Junit+Mockito

Mockito目前已經被集成到了springboot-test包中,只需要在工程的pom文件中引入spring-boot-starter-test就可以了,其中包括了junit和mockito類庫,這也是我們下面實戰中要用到的

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

寫junit本身是比較簡單的,最複雜的地方就在於寫mock類和對應的樁,關於什麼是樁的問題可以參考這篇博客單元測試中的驅動單元和樁單元的理解,同時如果不清楚mock是個什麼東西的話也可以參考Mock測試概念介紹那麼如何讓mock 啓動自動注入的bean和如何給mock類寫樁?

mock 自動注入

APMInfoServiceImpl是被測業務類,這個Service類具體業務邏輯不用考慮,只要知道這個被測類中有一個spring容器自動注入的APMInfoMapper的對象實例就可以了,下面就要對APMInfoServiceImpl中的APMInfoMapper實例做mock,並注入到APMInfoServiceImpl中

@Service
public class APMInfoServiceImpl implements APMInfoService {
    @Autowired
    private APMInfoMapper apmInfoMapper;

    @Override
    public List<HotAppTimeEntity> queryHotAppTime(int limit){
        ...
        return hotAppTimeEntityList;
    }
}

下面就是測試類,首先在測試類名前需要加上@RunWith(SpringRunner.class),表示該測試類運行的時候會先加載spring框架所需的相關類庫並將所有有註解的類進行自動依賴注入

在測試類中,需要在被測類對象聲明的時候加上@InjectMocks,就是將所有的mock類注入到這個對象實例中,注意這裏對APMInfoService的創建必須要通過new來初始化,不能像@Autowired那樣靠spring自動注入依賴類,因爲這裏APMInfoService內部依賴的類都是Mock的對象,必須要顯式創建類實例Mockito才能注入成功,這樣你就會發現在下面測試方法調用的時候被測類就不會再是null

@RunWith(SpringRunner.class)
public class APMInfoServiceImplTest {
    @InjectMocks
    private APMInfoService apmInfoService = new APMInfoServiceImpl();
    @Mock
    private APMInfoMapper apmInfoMapper;

    @Before
    public void setUpHotAppData() {
        //準備樁數據,queryHotAppTime mock 正常數據
        List<HotAppTimeEntity> hotAppTimeEntityList = new ArrayList<>();
        HotAppTimeEntity hopAppTimeEntity1 = new HotAppTimeEntity();
        //省略set方法調用
        HotAppTimeEntity hopAppTimeEntity2 = new HotAppTimeEntity();
        //省略set方法調用
        hotAppTimeEntityList.add(hopAppTimeEntity1);
        hotAppTimeEntityList.add(hopAppTimeEntity2);
        when(apmInfoMapper.queryHotAppTime(5, DateUtil.today())).thenReturn(hotAppTimeEntityList);

        HotAppTimeEntity hopAppTotal = new HotAppTimeEntity();
        hopAppTotal.setTotalNum(new Long(100));
        //寫樁方法
        when(apmInfoMapper.queryHotTotal(DateUtil.today())).thenReturn(hopAppTotal);

        //queryHotAppTime mock 空數據
        when(apmInfoMapper.queryHotAppTime(4, DateUtil.today())).thenReturn(null);
    }

    @Test
    public void queryHotAppTime() throws Exception {
        //正常數據
        List<HotAppTimeEntity> hotAppTimeEntityList = apmInfoService.queryHotAppTime(5);
        Assert.assertEquals("10001", hotAppTimeEntityList.get(0).getAppID());
        Assert.assertEquals(6.0, hotAppTimeEntityList.get(0).getAvgDuration(), 0.0000);
        Assert.assertEquals(0.8, hotAppTimeEntityList.get(1).getSuccessRate(), 0.0000);
        Assert.assertEquals(0.1, hotAppTimeEntityList.get(1).getRatio(), 0.0000);

        //null數據處理
        List<HotAppTimeEntity> hotAppTimeEntityNullList = apmInfoService.queryHotAppTime(4);
        Assert.assertNull(hotAppTimeEntityNullList);
    }

mock類寫樁

被測類和其中依賴的類已經通過Mockito創建好了,由於依賴的APMInfoMapper對象都是Mock出來的假數據,要讓測試能正常運行,還需要給APMInfoMapper被調用到的方法寫樁,就是常說的擋板,當方法的輸入A返回B。Mockito提供的樁方法也很簡單就是when(A).thenReturn(B)這樣的結構,when有很多種重載方法,具體如何使用建議參考Mockito的接口文檔

從這個例子中可以看到給apmInfoMapper寫了queryHotAppTime()和queryHotTotal()兩個樁方法,因爲在APMInfoServiceImpl中用到了這兩個方法,這樣當在測試類中調用APMInfoServiceImpl的queryHotAppTime方法時,方法內部使用了apmInfoMapper的queryHotAppTime()和queryHotTotal()的地方就會返回設置的兩個樁方法,並return我提前設置好的返回內容,這樣APMInfoServiceImpl的queryHotAppTime方法就不會報錯,並且我可以根據方法實現內部的邏輯設計不同的樁方法返回來覆蓋到所有的邏輯分支,最關鍵的是這個測試方法不再依賴其他任何一個類,唯一以來的類也自己實現了樁方法,並且大家可以發現不用把springboot的應用容器運行起來,所以測試速度非常快


4.項目實戰

新建項目導入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

目錄結構如下
在這裏插入圖片描述

新建被測試類Message、測試類TestMessage、以及運行文件TestRunner

package com.wxy.junit;

public class Message {
    private String message;

    public Message(String message){
        this.message = message;
    }

    public String printMessage(){
        System.out.println(this.message);
        return message;
    }
}

package com.wxy.junit;
import org.junit.Test;

import static org.junit.Assert.*;

public class TestMessage {
    private String message = "hello world";
    private Message newMessage= new Message(this.message);

    @Test
    public void testPrintMessage(){
        assertEquals(message,newMessage.printMessage());
    }
}
package com.wxy.junit;

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;

public class TestRunner {
    public static void main(String[] args) {
        Result result = JUnitCore.runClasses(TestMessage.class);
        for(Failure failure : result.getFailures()){
            System.out.println(failure.toString());
        }
        System.out.println("測試結果:"+result.wasSuccessful());
    }
}

運行結果如下,true表示運行成功:
在這裏插入圖片描述
那麼如果不成功呢?修改TestMessage文件如下:
在這裏插入圖片描述

運行結果如下,可以看出打印出來的爲false,信息期望結果是hello girl,結果是hello world
在這裏插入圖片描述


5.JUnit斷言

剛纔上面列舉了一個小栗子來闡述一下Junit測試的一個大概流程以及實現,那麼上面測試代碼中:assertEquals(message,newMessage.printMessage());是什麼意思呢?

其實這就是所謂的斷言,Junit所有的斷言都包含在 Assert 類中,可以把斷言理解爲是"判斷",這個類提供了很多有用的斷言方法來編寫測試用例,只有失敗的斷言纔會被記錄。Assert 類中的一些有用的方法列式如下:

  • void assertEquals(boolean expected, boolean actual):檢查兩個變量或者等式是否平衡
  • void assertTrue(boolean expected, boolean actual):檢查條件爲真
  • void assertFalse(boolean condition):檢查條件爲假
  • void assertNotNull(Object object):檢查對象不爲空
  • void assertNull(Object object):檢查對象爲空
  • void assertSame(boolean condition):assertSame() 方法檢查兩個相關對象是否指向同一個對象
  • void assertNotSame(boolean condition):assertNotSame() 方法檢查兩個相關對象是否不指向同一個對象
  • void assertArrayEquals(expectedArray, resultArray):assertArrayEquals() 方法檢查兩個數組是否相等

6.JUnit 註解

雖然上面的栗子中沒有全部用到這些註解,但是在這裏記錄說明一下:

  • @Test:這個註釋說明依附在 JUnit 的 public void 方法可以作爲一個測試案例
  • @Before:有些測試在運行前需要創造幾個相似的對象。在 public void 方法加該註釋是因爲該方法需要在 test 方法前運行;方法針對每一個測試用例執行,但是是在執行測試用例之前
  • @After:如果你將外部資源在 Before 方法中分配,那麼你需要在測試運行後釋放他們。在 public void 方法加該註釋是因爲該方法需要在 test 方法後運行;方法針對每一個測試用例執行,但是是在執行測試用例之後
  • @BeforeClass:在 public void 方法加該註釋是因爲該方法需要在類中所有方法前運行;方法首先執行,並且只執行一次
  • @AfterClass:它將會使方法在所有測試結束後執行。這個可以用來進行清理活動;方法最後執行,並且只執行一次
  • @Ignore:這個註釋是用來忽略有關不需要執行的測試的

JUnit 執行測試

測試用例是使用 JUnitCore 類來執行的。JUnitCore 是運行測試的外觀類。要從命令行運行測試,可以運行java org.junit.runner.JUnitCore。對於只有一次的測試運行,可以使用靜態方法 runClasses(Class[])


7.Junit套件測試

剛剛如上的例子只是對單個方法進行了測試,套件測試顧名思義就是捆綁幾個單元測試用例並且一起執行,在 JUnit 中,@RunWith和@Suite註釋用來運行套件測試,具體如下:

被測試TestMessage代碼不變如下:

package com.wxy.junit;

public class Message {
    private String message;

    public Message(String message){
        this.message = message;
    }

    public String printMessage(){
        System.out.println(this.message);
        return message;
    }
}

新建兩個測試類TestMessage、TestMessage1,用來展示套件測試,這裏我們之前新建了TestMessage,所以這裏在新建個TestMessage1,代碼如下:

package com.wxy.junit;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class TestMessage1 {
    private String message = "I am a boy";
    private Message newMessage= new Message(this.message);

    @Test
    public void testPrintMessage(){
        message = "I am a girl";
        assertEquals(message,newMessage.printMessage());
    }
}

同級目錄新建TestSuite代碼如下:

package com.wxy.junit;

import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({
        TestMessage.class,
        TestMessage1.class
})
public class TestSuite {
}

修改TestRunner運行類如下:

package com.wxy.junit;

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;


public class TestRunner {
    public static void main(String[] args) {
        Result result = JUnitCore.runClasses(TestSuite.class);
        for(Failure failure : result.getFailures()){
            System.out.println(failure.toString());
        }
        System.out.println("測試結果:"+result.wasSuccessful());
    }
}

運行結果如下:
在這裏插入圖片描述

8.Junit參數化測試

Junit 4 引入了一個新的功能參數化測試,參數化測試允許開發人員使用不同的值反覆運行同一個測試,可通過如下個步驟來創建參數化測試

  • 用 @RunWith(Parameterized.class) 來註釋 test 類。
  • 創建一個由 @Parameters註釋的公共的靜態方法,它返回一個對象的集合(數組)來作爲測試數據集合
  • 創建一個公共的構造函數,它接受和一行測試數據相等同的東西
  • 爲每一列測試數據創建一個實例變量
  • 用實例變量作爲測試數據的來源來創建你的測試用例

舉慄如下:

創建一個被測試類 NumberCheck

package com.wxy.junit;

public class NumberCheck {
    public boolean validate(final int primeNumber){
        for(int i=0;i<(primeNumber/2);i++){
            if(primeNumber%2==0){
                return false;
            }
        }
        return true;
    }
}

創建測試類 TestNumberCheck

package com.wxy.junit;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.Collection;

@RunWith(Parameterized.class)
public class TestNumberCheck {
    private int inputNumber;
    private boolean exceptResult;
    private NumberCheck numberCheck;

    public TestNumberCheck(int inputNumber,boolean exceptResult){
        this.inputNumber = inputNumber;
        this.exceptResult = exceptResult;
    }

    @Before
    public void initialize(){
        numberCheck = new NumberCheck();
    }

    @Parameterized.Parameters
    public static Collection primeNumbers(){
        return Arrays.asList(new Object[][]{
                {2,true},
                {8,false},
                {11,true},
                {24,false}
        });
    }
    @Test
    public void testPrimeNumberCheck(){
        System.out.println("Number is :"+inputNumber);
        assertEquals(exceptResult,numberCheck.validate(inputNumber));
    }
}

修改運行類 TestRunner 如下:

package com.wxy.junit;

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;

public class TestRunner {
    public static void main(String[] args) {
        Result result = JUnitCore.runClasses(TestNumberCheck.class);
        for(Failure failure : result.getFailures()){
            System.out.println(failure.toString());
        }
        System.out.println("測試結果:"+result.wasSuccessful());
    }
}

運行結果如下:
在這裏插入圖片描述

結語:以上就是關於Junit單元測試的一些基礎概念和栗子,有疑問或者問題歡迎留言,common progress

發佈了115 篇原創文章 · 獲贊 59 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章