干貨 | 精準(zhǔn)化測試原理簡介與實踐探索
小時候大家應(yīng)該都玩過一個游戲,游戲很簡單,就是找不同,在規(guī)定時間內(nèi)兩幅圖直接的差異點找到就算贏,越快越好,就像下面這樣:
上面這個不同點想找很簡單,那么下面這樣的呢?
這個,確實有的人會說"我可以!" 。比如在綜藝節(jié)目"最強(qiáng)大腦"中,這群"變態(tài)"的非人類確實可以
反正我不行,我也不信你們看到文章這里的人可以~我只有最菜大腦
理論上,我們?nèi)娴臏y試覆蓋,肯定就就可以保證,那么我們先看下下面的代碼:
這是一份涉及訂單狀態(tài)的各種枚舉,每一個狀態(tài)的背后都有其業(yè)務(wù)邏輯,甚至還有交叉,假若按照笛卡爾積或者正交的方式來進(jìn)行用例設(shè)計與覆蓋,有。。。好多好多用例


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

測試發(fā)展到如今,好像不會點自動化,都不好意思叫測試,簡歷上不寫點自動化都拿不出手,但是自動化真的是測試的銀彈不,做過的應(yīng)該深有感觸,自動化屬于一個奢侈品:
開發(fā)正本
維護(hù)成本
如何使用
用例的設(shè)計合理性
新功能的滯后性
再者,你確定你真的覆蓋到了被測代碼?也就是相當(dāng)于魔方墻上的每個色塊,實際在黑盒測試的過程中很大程度上取決于測試人員的經(jīng)驗,主觀性很強(qiáng),這樣就很可能漏測,發(fā)布后出了問題就又要開撕了。。。
可能有的小伙伴會這樣覺得,有人告訴我們答案,也就是告訴我們魔方墻的差異之處。這樣我不就知道關(guān)注的測試點了嗎?
沒錯,我們可以讓開發(fā)告訴我們本次改了哪些方法,甚至有代碼權(quán)限的情況下我們有能力可以自己去分析代碼,妥了,金女士!
那么問題又來了。針對上面的情況,開發(fā)的描述一定是正確全面的嗎?即使開發(fā)準(zhǔn)確的說明了改動的代碼,那么改動所影響到的其他范圍呢?開發(fā)本人也不好確認(rèn)的(不然還要測試干啥~),開發(fā)也有可能偷偷改代碼不告訴你呢。

這個時候就渴望有這么一個"最強(qiáng)大腦"

眼過去就可以看出差異點(本次改動的邏輯)
腦海中就有了差異的影響范圍(縮小需要測試的范圍)
再一掃就看出哪些測試覆蓋到了(確認(rèn)測試覆蓋率)
以求達(dá)到一種精準(zhǔn)測試的程度
按照上面的描述,大概我們可以分為三個維度:
差異化
調(diào)用鏈
覆蓋率 接下來的文章中會一個個詳細(xì)來說~
不同的語言,都會有對應(yīng)不同的語法分析器,語法分析器會把源代碼作為字符串讀入、解析,并建立語法樹,這是一個程序完成編譯所必要的前期工作。
我們看下 Java 的編譯過程,重點關(guān)注步驟一和步驟二:
這里我們使用一個簡單的Java對象,解析成AST后看下長什么樣子
由于層級太多太復(fù)雜,這里選取屬性user做個簡單演示說明。如下:
每一項里面都包含了最全面的信息,包括名稱、行號等,具體的可以訪問在線調(diào)試網(wǎng)站https://astexplorer.net/進(jìn)行調(diào)試查看
既然所有的代碼信息都有了,那么我們就可以拿著這些信息進(jìn)行比對,從而找出代碼的差異之處;(當(dāng)然這其中還是要很多降噪處理的,例如注釋、空格、業(yè)務(wù)無關(guān)代碼get/set等) 大概的流程邏輯如下
3.2.1 字節(jié)碼
因為Java代碼的運行,是通過javac先將Java文件編譯成.class結(jié)尾的字節(jié)碼,再由JVM去執(zhí)行;所以在字節(jié)碼文件中,擁有了足夠的元數(shù)據(jù)來解析類中的所有元素:類名稱、父類名、方法、屬性以及 Java 字節(jié)碼(指令);
以如下源碼為例:
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
命令將其編譯為字節(jié)碼文件,再使用 命令將其反編譯后得到如下信息:
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(); ? ? ? ? ? ?//構(gòu)造函數(shù)
? ?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; ? ? ?//方法描述符(入?yún)⒑头祷刂殿愋? ? ? ? ? ? ? ? ?
? ?flags: ACC_PUBLIC ? ? ? ? ? ? ?//方法的訪問標(biāo)致
? ?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中的行號做對應(yīng)
? ? ? ?line 6: 0
? ? ?LocalVariableTable:
? ? ? ?Start ?Length ?Slot ?Name ? Signature
? ? ? ? ? ?0 ? ? ? 7 ? ? 0 ?this ? LAccurateTest;
? ? ? ? ? ?0 ? ? ? 7 ? ? 1 ? ? b ? I ? ? ?//本地變量
}
SourceFile: "AccurateTest.java"
通過上述信息我們可以直觀的看到字節(jié)碼中包含了Java運行所需的所有信息,且JVM對于字節(jié)碼文件要求嚴(yán)格,必須按照固定的組成和順序,而這種特性也就適合利用訪問者模式對字節(jié)碼文件進(jìn)行修改;因此也就要介紹我們的調(diào)用鏈生成的核心技術(shù)?!狝SM
3.2.2 ASM
操作; API接口,每當(dāng) ,掃描到類注解就會回調(diào) 等; 方法來實現(xiàn)字節(jié)碼的讀取和插入,例如在做調(diào)用鏈分析時我們就用到了其 方法來對方法體內(nèi)的調(diào)用信息進(jìn)行過濾和提取
通過上述的信息進(jìn)行匹配橋接,我們就可以拿到調(diào)用鏈中的一系列父子節(jié)點,形成我們的方法調(diào)用鏈
大概的流程邏輯如下:
說到覆蓋率統(tǒng)計,就要介紹當(dāng)前在這個技術(shù)領(lǐng)域中占據(jù)主導(dǎo)地位的開源工具-jacoco jacoco使用總的來說和裝大象一樣,需要三步
對被測項目進(jìn)行字節(jié)碼插樁
覆蓋率數(shù)據(jù)的采集與導(dǎo)出
覆蓋率數(shù)據(jù)的統(tǒng)計與報告生成 下面我們對這三個步驟逐一拆解 插樁,其實就是安插監(jiān)控探頭,我們的一行行代碼就好比一條條馬路,代碼里的分支(if-else)就好比馬路上的各種支路岔道,而插樁就相當(dāng)于在每一條路的路口都裝上了一個探頭
如下就是在字節(jié)碼中插入探針信息的圖示:
jacoco的插樁模式有兩種:
on-the-fly模式(運行時插樁)
通過配置-javaagent在啟動命令中,jacoco介入被測項目部署過程,將探針(探頭)插入class文件,探針不改變原有方法的行為,只是記錄是否已經(jīng)執(zhí)行。
優(yōu)點:無需提前進(jìn)行字節(jié)碼插樁,無需考慮classpath 的設(shè)置。
缺點:要修改JVM參數(shù),對環(huán)境的要求比較高,于一些無法修改啟動命令的場景不適用。
offline模式(編譯時插樁)
在測試之前先對文件進(jìn)行插樁,生成插過樁的class或jar包,測試插過樁的class和jar包,生成覆蓋率信息到文件,最后統(tǒng)一處理,生成報告。
優(yōu)點:屏蔽工具對虛擬機(jī)環(huán)境的依賴;
缺點:需要提前侵入代碼;無法實時獲取覆蓋率,只能測試完成后停止項目后統(tǒng)一生成報告 選擇:
方式無須入侵應(yīng)用啟動腳本,再加上公司的運維和開發(fā)可以配合部署 啟動參數(shù),因此我們最終選擇 模式進(jìn)行插樁
3.3.2 覆蓋率收集與導(dǎo)出 看了上面的插樁原理,想必覆蓋率的收集也就很好理解了,依然是以監(jiān)控探頭為例,當(dāng)我們測試一行行代碼時,就相當(dāng)于開著車跑在一條條道路上,而每進(jìn)入一行代碼就像是開車進(jìn)入了一條道路,那么進(jìn)入的時候就會被監(jiān)控探頭拍攝記錄下來,也就知道你跑過哪條路了。 同理,覆蓋到一行代碼時,探針就會記錄下信息,最終也就知道了哪一行代碼被覆蓋到了
至于導(dǎo)出,覆蓋率的統(tǒng)計信息會通過暴露的服務(wù)端口(默認(rèn)6300)去獲取,導(dǎo)出一份以.exec結(jié)尾的文件,文件中包含了當(dāng)前的覆蓋率信息
通過對exec文件的解析,jacoco便可以獲取所有方法的探針信息,從而計算覆蓋率,并對代碼進(jìn)行染色輸出報告:
針對代碼的染色如下

紅色:代表未覆蓋
黃色:代表部分覆蓋,
綠色:代表完全覆蓋 在實際的使用場景中,我們可能還更關(guān)注本次修改的代碼,測試的時候我們會重點測試本輪開發(fā)的新增和改動范圍,因此jacoco原生的功能就不能滿足了,jacoco原生統(tǒng)計的是全量的覆蓋率。
對于改動點,我們稱之為增量,所以我們對jacoco的源碼進(jìn)行了二次開發(fā),使其支持增量的覆蓋率統(tǒng)計,以滿足日常測試需求;對比上面全量的范圍,可以看到增量的統(tǒng)計范圍就明確了,數(shù)量就少了很多:
大概的架構(gòu)邏輯如下:
開發(fā)修改了一個方法或者一個接口,那么這個接口可能被N個應(yīng)用去調(diào)用,一旦這個接口有問題,那么影響面是相當(dāng)大的;或者這個接口本身沒問題,但是上下游沒有兼容好,調(diào)用出了問題也是影響產(chǎn)品質(zhì)量的;所以這個也是我們測試關(guān)注的重點。 再者,我們?nèi)粘5臏y試有很大一部分比例是接口測試,包括自動化也是,接口自動化用例很多。那么如果可以通過調(diào)用鏈路找到本次修改所影響到的最上層的入口接口( 等),那么通過接口與用例的關(guān)聯(lián)關(guān)系,就可以推薦出本輪修改必須要執(zhí)行的用例,提高用例的精準(zhǔn)程度和更加明確的測試范圍。 還有,如果改動的接口沒有關(guān)聯(lián)的用例,或者用例執(zhí)行完以后覆蓋率不達(dá)標(biāo),那么也可以對用例進(jìn)行查漏,添加新的用例進(jìn)行覆蓋。
優(yōu)點:方案相對成熟,業(yè)界有落地案例,實現(xiàn)難度尚可
缺點:鏈路也是通過插樁監(jiān)控的,那么前提就是這條鏈路要走到了才會存在,這樣就有滯后性,新增加的代碼鏈路還沒有測試過,那這條鏈路自然也就拿不到了
聊到這里,基本上就把測試人員的靈魂3問給回答完畢了。關(guān)于精準(zhǔn)化測試,這里有幾個問題會困擾測試開發(fā)人員。這里給出一些建議,希望可以對讀者有所益處。 1、如果我的代碼覆蓋率達(dá)到100%了,是不是就可以說測試覆蓋完全了,質(zhì)量有保障了?
答:不是, 覆蓋率低,質(zhì)量一定沒有保障,但是覆蓋率高,只是保障的一個維度達(dá)到了。 這里我們只是知道了代碼被覆蓋了,但是代碼邏輯的正確性呢?精準(zhǔn)化是無法判斷的,要靠大家自己去斷言了。 再者,覆蓋到的代碼都是開發(fā)按照自己理解的業(yè)務(wù)邏輯寫的,如果他漏寫了一些需求邏輯呢?那這部分就不存在覆蓋的情況了。
2、我是不是每次都要保證所有的方法覆蓋率都達(dá)到100%?
答:不是,方法的覆蓋率要達(dá)到什么樣的一個值,不好直接下結(jié)論。有些代碼邏輯,好比一些異常的捕獲,這個異常的觸發(fā)場景很難,日常測試幾乎走不到,那么就是覆蓋不了,覆蓋率也就不可能達(dá)到100%。
3、根據(jù)問題2,既然達(dá)不到100%,那么我是不是設(shè)一個閾值,好比80%?90%?,達(dá)到這個閾值就可以了?
答:也不是,有些方法,它的代碼邏輯可能都是核心邏輯,其中的分支都需要覆蓋,缺少了就有漏測出Bug的風(fēng)險,且理論上都是可以通過測試覆蓋到的,那么這種方法就需要達(dá)到100%的覆蓋率。
4、那要怎么衡量覆蓋率的指標(biāo)?
答:一方面可以設(shè)定一個最低閾值,哪怕代碼有些邏輯走不到,也不會大面積并且占比很高,還是需要一個最低的覆蓋率保障; 再者,需要測試的同學(xué)根據(jù)自己測試的業(yè)務(wù)進(jìn)行情況劃分,具備codereview的能力和習(xí)慣,平臺僅作為一個輔助測試的工具; 最后,我們可以記錄下以往測試的覆蓋率,根據(jù)不同業(yè)務(wù)通過測試后的覆蓋率情況統(tǒng)計覆蓋率的趨勢,以歷史的覆蓋率數(shù)據(jù)為依據(jù)來設(shè)定閾值或監(jiān)控告警,如果覆蓋率低于往期正常的值,就進(jìn)行告警或者卡點