精準化測試原理簡介

小時候大家應該都玩過一個遊戲,遊戲很簡單,就是找不同,在規定時間內兩幅圖直接的差異點找到就算贏,越快越好,就像下面這樣:

上面這個不同點想找很簡單,那麼下面這樣的呢?

這個,確實有的人會說"我可以!" 。比如在綜藝節目"最強大腦"中,這羣"變態"的非人類確實可以

反正我不行,我也不信你們看到文章這裏的人可以~我只有最菜大腦

理論上,我們全面的測試覆蓋,肯定就就可以保證,那麼我們先看下下面的代碼:

這是一份涉及訂單狀態的各種枚舉,每一個狀態的背後都有其業務邏輯,甚至還有交叉,假若按照笛卡爾積或者正交的方式來進行用例設計與覆蓋,有。。。好多好多用例

  • 那麼~你真的有那麼多時間去全覆蓋嗎?
    開發:我改了點代碼,等會幫忙全面迴歸一遍吧
    測試:好的(*** bi~~ ***)
    什麼?自動化?Are you sure?

測試發展到如今,好像不會點自動化,都不好意思叫測試,簡歷上不寫點自動化都拿不出手,但是自動化真的是測試的銀彈不,做過的應該深有感觸,自動化屬於一個奢侈品:

  • 開發正本
  • 維護成本
  • 如何使用
  • 用例的設計合理性
  • 新功能的滯後性

再者,你確定你真的覆蓋到了被測代碼?也就是相當於魔方牆上的每個色塊,實際在黑盒測試的過程中很大程度上取決於測試人員的經驗,主觀性很強,這樣就很可能漏測,發佈後出了問題就又要開撕了。。。

可能有的小夥伴會這樣覺得,有人告訴我們答案,也就是告訴我們魔方牆的差異之處。這樣我不就知道關注的測試點了嗎?

沒錯,我們可以讓開發告訴我們本次改了哪些方法,甚至有代碼權限的情況下我們有能力可以自己去分析代碼,妥了,金女士!

那麼問題又來了。針對上面的情況,開發的描述一定是正確全面的嗎?即使開發準確的說明了改動的代碼,那麼改動所影響到的其他範圍呢?開發本人也不好確認的(不然還要測試幹啥~),開發也有可能偷偷改代碼不告訴你呢。

這個時候就渴望有這麼一個"最強大腦"

  • 眼過去就可以看出差異點(本次改動的邏輯)
  • 腦海中就有了差異的影響範圍(縮小需要測試的範圍)
  • 再一掃就看出哪些測試覆蓋到了(確認測試覆蓋率)

以求達到一種精準測試的程度

按照上面的描述,大概我們可以分爲三個維度:

  • 差異化
  • 調用鏈
  • 覆蓋率
    接下來的文章中會一個個詳細來說~

不同的語言,都會有對應不同的語法分析器,語法分析器會把源代碼作爲字符串讀入、解析,並建立語法樹,這是一個程序完成編譯所必要的前期工作。

我們看下 Java 的編譯過程,重點關注步驟一和步驟二:

這裏我們使用一個簡單的Java對象,解析成AST後看下長什麼樣子

由於層級太多太複雜,這裏選取屬性user做個簡單演示說明。如下:

每一項裏面都包含了最全面的信息,包括名稱、行號等,具體的可以訪問在線調試網站https://astexplorer.net/進行調試查看

既然所有的代碼信息都有了,那麼我們就可以拿着這些信息進行比對,從而找出代碼的差異之處;(當然這其中還是要很多降噪處理的,例如註釋、空格、業務無關代碼get/set等)
大概的流程邏輯如下

3.2.1 字節碼

因爲Java代碼的運行,是通過javac先將Java文件編譯成.class結尾的字節碼,再由JVM去執行;所以在字節碼文件中,擁有了足夠的元數據來解析類中的所有元素:類名稱、父類名、方法、屬性以及 Java 字節碼(指令);

以如下源碼爲例:

1  public class AccurateTest {
2
3     private int a = 1;
4
5     public String add(int b){
6        return String.valueOf(a + b);
7    }
8 }
9

命令將其編譯爲字節碼文件,再使用
命令將其反編譯後得到如下信息:

Classfile /Users/qinzhen/Documents/My/TrainingProject/calctest/src/test/java/AccurateTest.class
  Last modified 2021-7-15; size 386 bytes
  MD5 checksum e67842e9b540c556d288c28b303298fb
  Compiled from "AccurateTest.java"
public class AccurateTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#19         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#20         // AccurateTest.a:I
   #3 = Class              #21            // AccurateTest
   #4 = Class              #22            // java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LAccurateTest;
  #14 = Utf8               add
  #15 = Utf8               (I)I
  #16 = Utf8               b
  #17 = Utf8               SourceFile
  #18 = Utf8               AccurateTest.java
  #19 = NameAndType        #7:#8          // "<init>":()V
  #20 = NameAndType        #5:#6          // a:I
  #21 = Utf8               AccurateTest
  #22 = Utf8               java/lang/Object
{
  public AccurateTest();            //構造函數
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 1: 0
        line 3: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LAccurateTest;

  public java.lang.String add(int);        //方法名
    descriptor: (I)Ljava/lang/String;      //方法描述符(入參和返回值類型)                
    flags: ACC_PUBLIC              //方法的訪問標緻
    Code:                    //code開始
      stack=2, locals=2, args_size=2
         0: aload_0
         1: getfield      #2                    // 引用常量池的值 Field a:I
         4: iload_1
         5: iadd
         6: invokestatic  #3                    // Method java/lang/String.valueOf:(I)Ljava/lang/String;
         9: ireturn
      LineNumberTable:              //行號表,將上述操作碼與.java中的行號做對應
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   LAccurateTest;
            0       7     1     b   I      //本地變量
}
SourceFile: "AccurateTest.java"

通過上述信息我們可以直觀的看到字節碼中包含了Java運行所需的所有信息,且JVM對於字節碼文件要求嚴格,必須按照固定的組成和順序,而這種特性也就適合利用訪問者模式對字節碼文件進行修改;因此也就要介紹我們的調用鏈生成的核心技術棧——ASM

3.2.2 ASM

操作;
API接口,每當
,掃描到類註解就會回調
等;
方法來實現字節碼的讀取和插入,例如在做調用鏈分析時我們就用到了其
方法來對方法體內的調用信息進行過濾和提取

通過上述的信息進行匹配橋接,我們就可以拿到調用鏈中的一系列父子節點,形成我們的方法調用鏈

大概的流程邏輯如下:

說到覆蓋率統計,就要介紹當前在這個技術領域中佔據主導地位的開源工具-jacoco
jacoco使用總的來說和裝大象一樣,需要三步

    1. 對被測項目進行字節碼插樁
    1. 覆蓋率數據的採集與導出
    1. 覆蓋率數據的統計與報告生成
      下面我們對這三個步驟逐一拆解
      插樁,其實就是安插監控探頭,我們的一行行代碼就好比一條條馬路,代碼裏的分支(if-else)就好比馬路上的各種支路岔道,而插樁就相當於在每一條路的路口都裝上了一個探頭

如下就是在字節碼中插入探針信息的圖示:

jacoco的插樁模式有兩種:

  • on-the-fly模式(運行時插樁)
  • 通過配置-javaagent在啓動命令中,jacoco介入被測項目部署過程,將探針(探頭)插入class文件,探針不改變原有方法的行爲,只是記錄是否已經執行。
  • 優點:無需提前進行字節碼插樁,無需考慮classpath 的設置。
  • 缺點:要修改JVM參數,對環境的要求比較高,於一些無法修改啓動命令的場景不適用。
  • offline模式(編譯時插樁)
  • 在測試之前先對文件進行插樁,生成插過樁的class或jar包,測試插過樁的class和jar包,生成覆蓋率信息到文件,最後統一處理,生成報告。
  • 優點:屏蔽工具對虛擬機環境的依賴;
  • 缺點:需要提前侵入代碼;無法實時獲取覆蓋率,只能測試完成後停止項目後統一生成報告
    選擇:

方式無須入侵應用啓動腳本,再加上公司的運維和開發可以配合部署
啓動參數,因此我們最終選擇
模式進行插樁

3.3.2 覆蓋率收集與導出
看了上面的插樁原理,想必覆蓋率的收集也就很好理解了,依然是以監控探頭爲例,當我們測試一行行代碼時,就相當於開着車跑在一條條道路上,而每進入一行代碼就像是開車進入了一條道路,那麼進入的時候就會被監控探頭拍攝記錄下來,也就知道你跑過哪條路了。
同理,覆蓋到一行代碼時,探針就會記錄下信息,最終也就知道了哪一行代碼被覆蓋到了

至於導出,覆蓋率的統計信息會通過暴露的服務端口(默認6300)去獲取,導出一份以.exec結尾的文件,文件中包含了當前的覆蓋率信息

通過對exec文件的解析,jacoco便可以獲取所有方法的探針信息,從而計算覆蓋率,並對代碼進行染色輸出報告:

針對代碼的染色如下

  • 紅色:代表未覆蓋
  • 黃色:代表部分覆蓋,
  • 綠色:代表完全覆蓋
    在實際的使用場景中,我們可能還更關注本次修改的代碼,測試的時候我們會重點測試本輪開發的新增和改動範圍,因此jacoco原生的功能就不能滿足了,jacoco原生統計的是全量的覆蓋率。

對於改動點,我們稱之爲增量,所以我們對jacoco的源碼進行了二次開發,使其支持增量的覆蓋率統計,以滿足日常測試需求;對比上面全量的範圍,可以看到增量的統計範圍就明確了,數量就少了很多:

  • 大概的架構邏輯如下:

開發修改了一個方法或者一個接口,那麼這個接口可能被N個應用去調用,一旦這個接口有問題,那麼影響面是相當大的;或者這個接口本身沒問題,但是上下游沒有兼容好,調用出了問題也是影響產品質量的;所以這個也是我們測試關注的重點。
再者,我們日常的測試有很大一部分比例是接口測試,包括自動化也是,接口自動化用例很多。那麼如果可以通過調用鏈路找到本次修改所影響到的最上層的入口接口(
等),那麼通過接口與用例的關聯關係,就可以推薦出本輪修改必須要執行的用例,提高用例的精準程度和更加明確的測試範圍。
還有,如果改動的接口沒有關聯的用例,或者用例執行完以後覆蓋率不達標,那麼也可以對用例進行查漏,添加新的用例進行覆蓋。

  • 優點:方案相對成熟,業界有落地案例,實現難度尚可
  • 缺點:鏈路也是通過插樁監控的,那麼前提就是這條鏈路要走到了纔會存在,這樣就有滯後性,新增加的代碼鏈路還沒有測試過,那這條鏈路自然也就拿不到了

聊到這裏,基本上就把測試人員的靈魂3問給回答完畢了。關於精準化測試,這裏有幾個問題會困擾測試開發人員。這裏給出一些建議,希望可以對讀者有所益處。
1、如果我的代碼覆蓋率達到100%了,是不是就可以說測試覆蓋完全了,質量有保障了?

答:不是, 覆蓋率低,質量一定沒有保障,但是覆蓋率高,只是保障的一個維度達到了。
這裏我們只是知道了代碼被覆蓋了,但是代碼邏輯的正確性呢?精準化是無法判斷的,要靠大家自己去斷言了。
再者,覆蓋到的代碼都是開發按照自己理解的業務邏輯寫的,如果他漏寫了一些需求邏輯呢?那這部分就不存在覆蓋的情況了。

2、我是不是每次都要保證所有的方法覆蓋率都達到100%?

答:不是,方法的覆蓋率要達到什麼樣的一個值,不好直接下結論。有些代碼邏輯,好比一些異常的捕獲,這個異常的觸發場景很難,日常測試幾乎走不到,那麼就是覆蓋不了,覆蓋率也就不可能達到100%。

3、根據問題2,既然達不到100%,那麼我是不是設一個閾值,好比80%?90%?,達到這個閾值就可以了?

答:也不是,有些方法,它的代碼邏輯可能都是核心邏輯,其中的分支都需要覆蓋,缺少了就有漏測出Bug的風險,且理論上都是可以通過測試覆蓋到的,那麼這種方法就需要達到100%的覆蓋率。

4、那要怎麼衡量覆蓋率的指標?

答:一方面可以設定一個最低閾值,哪怕代碼有些邏輯走不到,也不會大面積並且佔比很高,還是需要一個最低的覆蓋率保障;
再者,需要測試的同學根據自己測試的業務進行情況劃分,具備codereview的能力和習慣,平臺僅作爲一個輔助測試的工具;
最後,我們可以記錄下以往測試的覆蓋率,根據不同業務通過測試後的覆蓋率情況統計覆蓋率的趨勢,以歷史的覆蓋率數據爲依據來設定閾值或監控告警,如果覆蓋率低於往期正常的值,就進行告警或者卡點

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章