基於mockito作有效的單元測試

概述


本文講解的主要是有效和單元的思想,並非說如何編寫單元測試,用於改善和提升開發效率、編碼風格、編碼可讀性和單測效率,不盲目追求覆蓋率。java

背景


  • 如今不少單元測試只是利用@Test註解把代碼或者整個請求接口內的business作測試redis

  • 單測的過程就不少查數據庫的方法,可是不必每次都測sql,由於sql測一遍都應該是正確的。spring

  • 未明確單元測試由開發負責。單元測試是用於維護代碼邏輯不被修改或者,修改了也不出錯,不是測試的事情。sql

  • 單測代碼啓動速度、效率過低數據庫

  • 沒有在各個環境整個工程單元測試經過json

  • 方法寫的很大,行數不少,邊update邊作邏輯設計模式

  • 不少公司盲目追求覆蓋率網絡

目的

  • 單元測試啓動效率提高
  • 脫離環境,在每一個環境都放心執行,不要考慮測試環境、生產環境有沒有這條數據
  • 維護方法核心邏輯在後續的迭代中不被錯誤地修改
  • 用於改善和提升開發效率、編碼質量、編碼可讀性、減小冗長的代碼行

什麼是有效的單元測試


主要關鍵字: 有效、單元框架

  • 有效
  • 單元
  • 覆蓋場景
  • 可重複執行
  • 斷言
  • 不追求覆蓋率

1.有效的定義

  • 只測試核心的業務邏輯,如計算邏輯類(指標計算)、用戶支付下單組裝數據庫對象和子表(檢驗金額)、建立採買計劃表和detail表的對象(校驗指標個數、金額)
  • 不測用戶交互的,如導出數據excel的底色、查詢數據庫記錄
  • 不測邏輯的前置校驗
  • 不要測試明顯有用的東西。避免測試來自第三方供應商的類,特別是提供編寫代碼的框架的核心API的類。例如,不要測試向供應商的Hashtable類添加項、redis鎖等第三方庫
  • 不測環境相關的,儘可能脫離環境,能讓單測代碼在每一個環境都能正確執行。

2.單元的定義

  • 「單元測試」中的「單元」的意思是,將每一個單元設爲原子和儘量獨立。
  • 目的:方法小,則可能組件化地再次利用,維護邏輯在之後的迭代中不被錯誤的修改。若被錯誤修改,只要每次迭代都走全工程的單測時將會報錯而被發現。
  • 使得編寫代碼時方法職責單一,方法功能要小,方法邏輯小。也不要寫多行數方法,以多個小方法組成一個長邏輯。
  • 單測時不要寫大測試
  • 邏輯代碼方法儘可能不互相依賴。沒有關於測試執行順序的假設。
  • 邏輯代碼方法適當的小,則能容易安裝/拆卸
  • 單測時不必把全流程都測一遍,也就是沒必要從controller遇到的第一個business入口測。只測核心邏輯,也就是核心的每一個小方法的測試。

樣例:分佈式

public void updateUser(String userName,Integer age) {
        // 校驗
        if (StringUtils.isEmpty(userName)) {
            throw new RuntimeException();
        }
        // redis分佈式鎖
        getRedis();
        // 查詢主表
        selectUser();
        // 查詢出採買計劃Detail
        selectUserDetail();
        // 對數據進行處理
        calculateUserDetailInfo();
        // 更新主表
        updatePuchasePlanDb();
        // 更新detail表
        updatePuchasePlanDbDetail();
    }
複製代碼

假設上述updateUser是一個Controller下的第一個business邏輯入口,用於更新用戶信息,不少人的單元測試就會從updateUser這個方法開始作單元測試。可是這樣就不符合有效、單元的理念。

無心義測試

  • 由於如redis這些第三方庫,咱們是相信他是正確的,並且走redis是須要鏈接真實網絡,因此就會依賴環境,萬一網絡不通,單元測試這段代碼就沒法經過。
  • 前置校驗也不用測,由於錯誤率和之後改動的機會比較少
  • 查詢數據庫主表、detail表、更新也不用測試,由於都是SQL,單元測試是爲了維護邏輯不變,這些sql寫好一遍能正確,之後的迭代中都是正確的。

有意義測試

  • 因此咱們單測的時候只測數據處理的邏輯,假設數據處理邏輯以下,則對下面的3個方法每一個作一個單元測試。
  • 咱們會認爲組件式的方法能正確,組合起來就大機率正確。由於方法足夠的單元,則邏輯可插拔。
  • 若從頭測到尾,之後迭代中邏輯改動的機率很大,斷言錯誤機率大,這樣單元測試維護的意義就很小。
private void calculateUserDetailInfo() {
        // 更改公司與關聯上下級關係
        changeCompany();
        // 更改組與關聯上下級關係
        changeGroup();
        // 更改部門與關聯上下級關係
        changeDeptment();

    }
複製代碼

錯誤編碼案例:

private void calculateBrand() {
        for (int i = 0; i < new ArrayList<>().size(); i++) {

            for (int i1 = 0; i1 < new ArrayList<>().size(); i1++) {
                if (new Integer(1)==1) {
                    
                }  else {
                    
                }
            }
        }
        
    }
複製代碼

這樣的代碼就不符合單元的概念,至少把第二個for循環寫在另一個方法裏,由於單元測試中,認爲測試循環中一次是正確的,就斷言循環中每次都大機率正確。

3.覆蓋場景

  • 除了測試正常流程,還要測異常流程
  • 覆蓋方法正常運行,爲其建立一個單測
  • 覆蓋if else,爲同一個方法建立第二個單測

案例一 異常流程

public Integer logic2(Integer num) throws Exception {
        try {
            num = this.purchasePlanDetailBusinessImpl.method(num);
        } catch (RuntimeException e) {
           throw new Exception();
        }
        return ++num;
    }
複製代碼

則對應編寫:

/** * 測試異常場景 */
    @Test(expected = Exception.class)
    public void testMethodOnException() {
        Integer method = this.purchasePlanBusinessImpl.logic(null);
    }
複製代碼

4.斷言

不少人的單元測試都是最後print一下,在console裏面看日誌或者debug看看是否正確,可是這樣不足夠。

  • 應有對方法返回值、對象裏面的成員屬性作判斷
  • 判斷是否爲空或者判斷金額數值或者判斷異常是否符合預期
  • 只測試一件事一次。只要1個斷言測試一或者多個特性/行爲

以下,而不是直接判斷是否爲空或輸出到終端

@Test
    public void testMethodOnNormal() {
        Integer method = this.purchasePlanBusinessImpl.method(1);
        
        Assert.assertTrue(method == 2);
    }
複製代碼

5.可重複執行

  • 目前不少操做數據庫,或者內存數據庫
  • 操做真實數據庫,有可能下次就不能被斷言成功了,由於數據有可能被update
  • 內存數據庫雖然能夠下次啓動依然恢復默認的數據,可是有可能被其餘人的單測操做過,致使本身的數據被錯誤修改

6.不盲目追求覆蓋率

  • 不少公司盲目追求覆蓋率,說美國google覆蓋多高,可是別人工資高,6點準時下班,有足夠時間開發,開發前uml設計好每一個方法的入參出參,在中國是沒有這麼多時間,都是追求快速
  • 在中國大廠中,雖然有覆蓋率要求,可是覆蓋率只能證實你有沒有寫單元測試,可是單元測試寫的好很差,就是另一回事。由於有些人爲了提升覆蓋率,把一些無關的代碼也去單元測試,如測試controller層、測試entity構造函數、utils工具類等。
  • 螞蟻金服做爲樣例,螞蟻是根據項目分覆蓋級別,也不是全部的項目都要覆蓋率多高,通常50%就很高了,沒必要都過50%,本文主要推崇的是有效、單元。
  • 按照本文的有效、單元的作法,是會犧牲覆蓋率的,由於咱們只測核心邏輯
  • 不浪費時間,由於中國互聯網的迭代時間很是短,因此沒必要盲目爲覆蓋率而寫單元測試,單元測試的目的是提供代碼質量

操做工具


  • mockito:通常都能適用
  • powermock:在mockito的基礎上,能測試private方法,還能mock static靜態方法
  • @InjectMocks 用於框架new出對象,不用手動new 主要測試類
  • @Mock:被設置假對象返回的調用類
  • Assert.assertTrue等斷言
  • JSONObejct、JSONString:有時本身mock對象須要本身new ,可是這個時候咱們能夠寫好json體轉換成bean會比較方便。
<dependencies>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<version>4.3.7.RELEASE</version><!--$NO-MVN-MAN-VER$ -->
			<scope>test</scope>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.mockito/mockito-all -->
		<dependency>
			<groupId>org.mockito</groupId>
			<artifactId>mockito-all</artifactId>
			<version>1.10.19</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.mockito</groupId>
			<artifactId>mockito-core</artifactId>
			<version>2.7.12</version>
			<scope>test</scope>
		</dependency>
		<!-- https://mvnrepository.com/artifact/junit/junit -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>4.3.7.RELEASE</version><!--$NO-MVN-MAN-VER$ -->
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
			<version>4.3.7.RELEASE</version><!--$NO-MVN-MAN-VER$ -->
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.8</version>
			<scope>provided</scope>
		</dependency>
	</dependencies>
複製代碼
@RunWith(MockitoJUnitRunner.class)
public class PurchasePlanBusinessImplTest {
    
    @InjectMocks
    private DoSomeBusinessImpl purchasePlanBusinessImpl;
    @Mock
    private DoSomeDetailBusinessImpl purchasePlanDetailBusinessImpl;

    /** * 正常流程 */
    @Test
    public void testMethodOnNormal() {
        Integer method = this.purchasePlanBusinessImpl.method(1);
        
        Assert.assertTrue(method == 2);
    }

    /** * 在某些場景 */
    @Test
    public void testMethodOnXX() {
        when(this.purchasePlanDetailBusinessImpl.method(anyInt())).thenReturn(1);
        Integer method = this.purchasePlanBusinessImpl.logic(1);
        
        Assert.assertTrue(method == 3);
    }
複製代碼

影響力


效率提高

  • 目前測試要麼啓動服務、要麼單測啓動spring,其實都須要時間,不管是10幾秒仍是幾分鐘都是比較久的
  • 若是用mock方法,都是不須要基於sring容器,不須要自動注入就不須要解析bean關係,也不須要鏈接zk等環境問題。啓動時間只須要1秒
  • 不須要在真實數據庫造數據、不須要在內存數據庫寫sql
  • 若是測試整個工程的全部單測時,每個類單元測試都會加載一次spring、內存數據庫,致使跑整個工程都好久(實際上測試環境、迴歸環境都須要跑單測)

脫離環境

  • 其實每一個環境,不管開發聯調、測試、回顧、甚至生產環境都在構建時執行單元測試
  • 可是由於使用spring容器啓動的方式,每一個類的單測都須要啓動spring,致使執行時間過長
  • 也或者由於環境出現造數據問題,致使執行不成功
  • mock出來的數據都是在代碼實現,運行於內存中,因此不依賴中間件

方法小、行數少、職責小

  • 職責小:因單元測試的規則,讓寫每一個方法時,都有意識地寫的少依賴,這樣的方法就提示被其餘邏輯複用的概覽,而不是大方法,致使要用差很少的邏輯時,其餘同事就會去複製一份代碼。(例如不會在一個方法又作查詢數據庫、校驗、計算)
  • 提升方法被其餘邏輯利用的機率,由於大方法很難重複利用

規範

  • 單元測試類以被測類的實現類爲基礎加Test,如xxxBusinessImpTest
  • 單元測試類須要放在test目錄下,而且包名與被測類的路徑一致,防止idea、sonar跑覆蓋率校驗時,沒覆蓋到對應的方法。
  • 方法命名:test開頭,加真正的方法名,加場景,如testMethodOnNormal、testMethodOnException、testMethodOnLackOfMoney
  • 方法通常狀況下都帶返回值,即便沒有返回值也寫boolean。
  • 如果流程性方法等,能夠爲void,如責任鏈設計模式時
  • 必須斷言,而不是print。斷言值、正確與否、預期異常
  • 儘量把查數據的邏輯寫在被測方法以前,被測方法只作業務邏輯處理,不作查數據,這樣單測時須要mock的方法會比較少。
  • 公司技術體系不必定會預留單元測試時間,不多公司願意花時間在質量和維護上。都是趕着完成任務。
  • 例如3天開發,1天單元測試,1天sonar+review,1天聯調,不必定有時間作得完善。
相關文章
相關標籤/搜索