長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

DevOps訂閱號發表於2021-05-10


長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

來源:百度智慧化測試

作者:車釐子和夾心餅乾

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

導讀:線上系統異常問題一直以來都是使人”聞風喪膽”的,傳統手段在解決這類問題時面臨著相應的技術瓶頸。基於此,探索基於單元測試召回異常問題的方法,實現了一套通用且無人參與的單測生成系統,在百餘模組上落地取得了一定的效果。從近程式碼手段的單元測試著手,圍繞基於單測生成技術召回異常問題的應用實踐展開。主要介紹該方案0到1的整體建設思路、並從理解程式碼、構造高覆蓋測試用例資料、生成測試用例程式碼以及分析失敗用例這四方面展開介紹。

線上穩定性問題一直以來是備受大家關注的。在影響產品收益或使用者體驗的同時,也影響著QA的口碑。

為了避免這類問題發生線上上,測試人員有一系列的異常測試召回手段可以採取,常見的有:基於壓力測試、基於功能測試、基於單元測試或者基於靜態掃描的異常召回手段,然而在足夠完備的召回能力下,還是會有問題漏出到線上,這又是為什麼呢?

本文提出一種基於白盒手段召回異常問題的通用方法,並以C/C++語言為例,介紹該方法在百度服務端的落地思路。

undefined


一、問題分析

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF



帶著上面的疑問,本文對業界內現有異常測試手段在高成本、低召回這兩個問題維度上進行了對比分析,對比結果如下表所示。 

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

業界常用異常測試手段缺點對比分析

整體上看,當前的召回手段存在滯後性或成本高的問題,像基於壓力測試的異常召回手段資源消耗較高,基於功能測試的異常召回手段除開發成本較高外,還存在異常場景不易構造等滯後性問題。基於單元測試和靜態檢查來發現程式碼問題已是這些手段中缺點相對較少的,接下來更詳細地對比下單元測試和靜態檢查這兩種召回異常的手段。

如今,靜態檢查已是最常見的異常發現手段,其接入成本低,輕量級。以靜態掃描方式來檢查,不需要編譯執行、不佔用資源。但其存在以下問題:

  • 滯後性:線上出了問題後才能轉化為規則規避同類問題復發。

  • 準召低:規則靠人來設計,對某些場景會存在漏掉或者誤報的問題,需要case by case的解決。

  • 不可持續:缺少圍繞規則的生態建設,可能出現規則重複開發、缺少規則貢獻者、規則上線後無法有效評估等問題。

基於單元測試來召回異常問題有兩個缺點:開發成本高、依賴人的意識。開發人員針對本次功能中的重要函式,編寫其對應的單元測試程式碼來進行測試。選擇哪些函式,驗證哪些異常場景都依賴開發人員的經驗和主動程度。但它也有以下優點:

  • 測試最小單元,易於構造資料,驗證正確性。

  • 便於後續功能迴歸。

  • 資源消耗小。

  • 能更早發現問題,定位和解決成本低。

經過上述分析後,可以得出基於單元測試的優勢遠大於其缺點的結論,於是大膽假設:能否最大化它的優勢,解決依賴『寫』和『經驗』的問題。——即自動撰寫異常的單元測試程式碼,主動發現程式碼健壯性問題。

在這一設想下,提出一種可持續的、主被動結合、高ROI的穩定性問題召回漏斗(如圖所示),智慧UT作為靜態程式碼檢查環節後的主動召回手段,動態分析召回問題。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(穩定性召回漏斗圖)


二、解決思路

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF



2019年初,在對生成用例程式碼的強訴求下,調研了C/C++語言業界中比較通用且優秀的單測生成工具:C++ test和Wings,在開發成本、召回能力和是否開源三方面進行了對比,對比結果如下表所示。顯然,無論是C++ test還是Wings,都無法滿足業務線在複雜業務場景下完全自動化對複雜型別函式生成單測程式碼的訴求以及擴充套件,因此需要自建單測程式碼生成能力。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(業界C/C++單測生成工具調研對比 )

將開發人員對一個函式撰寫單元測試程式碼的過程進行拆解,各關鍵步驟如圖所示,將整個過程抽象為 “確認待測試函式->分析程式碼->構造測試資料->生成測試程式碼” 這4個過程。

  • 確認待測試函式:本次提交的程式碼中,並非所有的變更或新增函式都需要測試,可以結合函式屬性(如建構函式、解構函式等)、修改內容(如測試相關的程式碼、日誌邏輯等無風險函式)。

  • 分析程式碼:彙總被測函式的程式碼,如引數(輸入引數、內部依賴其它依賴引數)、返回值等資訊。

  • 構造測試資料:主動構造被測函式所需的用例資料,無需人工參與。

  • 生成測試程式碼:主動生成測試被測函式的程式碼,無需人工參與。

解決思路的關鍵是透過程式碼分析等白盒技術來實現一鍵異常單元測試程式碼的能力,真實模擬開發人員撰寫單元測試程式碼。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(開發人員實現單測程式碼編寫的過程)


三、實現方案

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF



基於上一節分析,整個技術方案設計如圖所示,本節重點介紹程式碼分析、測試用例生成、程式碼生成能力和執行分析的實現思路。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(技術方案設計圖)

3.1 程式碼分析能力

程式碼分析的目標是期望能透過靜態程式碼掃描的手段,將複雜的函式程式碼抽象成結構化的函式特徵資料,可以類比編譯符號表。基於這份結構化資料能直接感知函式呼叫方式、變數宣告和賦值方式等行為。

3.1.1 程式碼特徵

C/C++語言中,尤其是C++這類物件導向的語言,函式呼叫和類的宣告建立方式和普通變數不同,存在更豐富的語法多樣性。首先要明確該語言在程式碼分析過程中需要獲得的資訊內容,重點考慮的因素如下:

  • 函式呼叫:普通函式呼叫、類的成員函式呼叫 在呼叫類的普通成員函式前,需要先例項化類的物件,而非成員函式可直接呼叫。

  • 變數宣告實現:普通變數、class或struct變數、stl變數等;不同變數宣告賦值方式都不同,需要能夠區分是普通變數還是class、struct、stl變數。

  • 修飾符:const、static、virtual、inline等,加了修飾符的變數或函式會影響它的呼叫、例項化、賦值方式。

  • 檔案級別資訊:標頭檔案,名稱空間,標頭檔案和名稱空間不全或者缺失會影響測試程式碼的編譯。

  • 其它:一些影響賦值、例項化語法的其它屬性。如類是否禁用了複製/賦值建構函式等。

基於上述思路,最初敲定獲取如下程式碼特徵資訊:

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(程式碼特徵資訊)

3.1.2 特徵儲存

將特徵儲存以xml檔案格式儲存,儲存為程式碼結構資料(CodeStructData,CSD),且保證周邊模組能基於該份產出獲取函式呼叫方式、變數宣告和賦值方式。根據不同型別和賦值方式約定schema,如type、baseType1、parmType等屬性,Demo如圖配置示例。

  • type:實際型別。 

  • baseType1:該變數實際屬於類別,如內建型別、陣列型別、STL型別等。

  • parmType:宣告型別,生成程式碼時可直接取該欄位作為變數的宣告型別。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(經原始碼分析得到的CSD配置樣例) 

3.1.3 特徵採集

這一環節,期望能在不編譯的條件下,以靜態程式碼掃描方式提取程式碼資訊,且工具要輕量、高效、支援開源,以便於後續需求迭代。

在綜合對比下,最終選擇cppcheck,一個開源的靜態程式碼檢查工具,除此之外還可以基於它的符號表來做二次開發。為了採集函式呼叫鏈資訊和其它全域性資訊,內部對cppcheck進行了改造。採集過程如下圖所示。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(基於cppcheck的程式碼分析方案示意圖)

3.2 用例資料生成能力

3.2.1 解決思路

用例資料生成能力屬於Fuzzing技術領域中關鍵的一個環節,常見的fuzz資料手段有基於生成的和基於變異的兩種方式。一般會使用覆蓋率來衡量fuzz能力,比如函式覆蓋、行覆蓋或分支覆蓋。

  • 基於變異法:根據已知資料樣本透過變異的方法,生成測試用例。比如著名的AFL-fuzz技術,其主要處理過程如下圖所示:

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(AFL-Fuzz處理過程)

  • 基於生成法:根據已知協議或介面規範進行建模,生成測試用例。比如libfuzzer可以在不指定初始資料集下,透過被測目標的介面型別,隨機生成位元組資料,餵給被測目標。在生成用例資料時,避免用例爆炸也是生成的條件之一,過多的用例會存在用例無效和執行時效低等問題。

本文在傳統的基於生成法構造用例資料的基礎上,除了被測目標介面協議外,充分利用路徑和分支資訊來指導fuzz資料,覆蓋更多分支內的邏輯,還引入了其它白盒特徵,如變數擴散關聯性等去降低對無效用例的生成,最後以函式覆蓋和分支覆蓋作為fuzz能力的度量指標。

解決思路如下圖所示,資料生成層由CSD處理模組、路徑選擇模組、引數選擇模組和生成&篩選 模組構成。針對不同型別的變數,選取不同的異常候選集,生成初始用例集合,再經過用例篩選策略得到最終的測試用例集。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(測試用例生成方案示意圖)

3.2.2 路徑選擇

路徑選擇模組包含表示式約束求解、路徑可達分析以及路徑合併。這一部分的目的是指導資料生成對分支的覆蓋。路徑的提取,主要透過遍歷上一節提取的程式控制流資料來完成,可以採取深度最佳化遍歷或廣度優先遍歷,不影響結果。

為了避免路徑爆炸,可以先提取出期望測試覆蓋的目標,遍歷時每次選擇一個可以覆蓋待測試目標的路徑。

  1. 約束求解是指對路徑上的分支表示式進行求解計算,分別計算出表示式為真和假時的符號值。這裡需要先對錶達式進行替換,例如將函式呼叫替換成變數,便於計算。替換後的表示式可以使用開源的庫進行求解,如z3。

  2. 路徑可達分析是指以分支如if、while、for、switch為節點,計算節點內求解出的變數值或變數範圍,對函式內部各節點進行連線後,得到一個圖。結合每個節點變數的範圍,對圖中的路徑進行剔除,刪掉不可達的路徑。

  3. 路徑合併是指將含有交集的節點合併成一條路徑,減少後續用例生成數量。如圖程式程式碼示例中對_index_i和_index_j構造用例時,構造出{_index_i=1, _index_j=2}來滿足同時覆蓋17行和22行兩個分支的資料。在處理時需要分析出分支內部是否存在return、continue、break這類的跳轉或返回關鍵字,避免出現badcase。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(程式程式碼示例)

3.2.3 候選資料來源

各型別的候選異常資料可分為靜態資料和動態資料。

  • 靜態資料指透過歷史經驗維護的一份型別邊界值和業務邊界值資料庫。

  • 動態資料指透過業務資料採集和變異演算法,基於模組日誌、流量等資料來源透過插樁的方式挖掘出的業務值或經過變異得到的異常邊界值。

3.2.4 用例生成&篩選

基於上述步驟得到各引數候選值集合後,便可對引數之間進行組合,得到用例集合,引數組合的方式直接影響著用例量級,此階段重點考慮如何避免用例爆炸,減量不減質量。

經統計,大約70%以上的軟體問題是由一個 或2個引數作用引起的。因此引數因子兩兩組合就成為了軟體測試中一種實施性較強同時又比較有效的方法。如果採用全排列組合方式,在某業務場景下,某類classA型別作為函式形參,假設該classA有1000個成員變數,其成員變數全部為v型別,型別v有4個取值,v=[-1,0,1,-2147483649],那麼全排列組合後的用例資料量高達4^1000個。

可見,單純的全排列組合能保證當前因子組合覆蓋的場景最豐富,但會面臨case爆炸問題,這不符合實際應用背景。

其實,生成一個最小測試用例集是一個NPC問題,因此學術界一般是將找到一個儘可能小的測試用例集去覆蓋所有可能的配對來作為研究目標。本文先後使用兩個步驟來減輕用例的量級。

1)剔除無用屬性:基於程式碼分析減少對無用屬性的資料構造。透過分析自定義型別引數其成員屬性擴散性,只對類/結構體中實際被用到的成員屬性構造資料。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(函式內變數和其成員變數樣例)

2)剔除冗餘用例:採用基於生成的方式,選擇一種引數組合演算法,生成合適的測試用例。常見的生成技術大體可分為組合設計法、啟發式演算法、元啟發探索法:

  • 組合設計法,一般是圍繞正交表或其它代數的思路生成測試用例。

  • 啟發式演算法,一般是逐條地或逐因素擴充套件地生成測試用例。如經典的AETG演算法:首先按貪心演算法生成一定數量N個測試用例,然後從這N個測試用例中選擇一個能最多覆蓋未覆蓋配對集合中引數對的用例,將這個用例新增進已經形成的測試用例集T中,直至達到覆蓋目標。如IPO演算法,透過先水平、再垂直的方式擴充用例。

  • 元啟發式演算法,如遺傳演算法、模擬退火、蟻群演算法等,大致過程如下圖所示。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(元啟發式用例探索大致過程)

啟發式和元啟發式都屬於區域性搜尋演算法,不能保證最優,但可以保證處理時間。還可以將逐條生成法和元啟發方式結合,引入錯誤風險係數、組合約束和引數優先順序等資訊,進一步最佳化組合方式。

本文初期利用逐條生成的方式,基於基礎的成對法來減少重複無效的輸入。以一個例子簡單介紹本文使用的2-Wise testing成對法思路(其原理可參考文末提供的資料):假定有三個輸入變數,X、Y、Z,取值分別為D(X)={x1, x2, x3},D(Y)={y1,y2},D(Z)={z1,z2};

如果用全排列法,得到的測試用例集有3 X 2 X 2 = 12個用例,具體測試用例如下左圖所示,透過2-Wise testing處理後僅獲得6個用例。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(全排列用例和成對法演算法過程)

本文透過上述方法,有效剔除了90%以上的無用測試用例資料。最終將保留下來的測試用例以json格式儲存,作為測試資料集合,方便擴充套件和供其它場景使用。資料Demo如下所示,以函式名、func_data、變數名作為key,以具體的引數值作為value。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(資料Demo)

當前這種生成方式是以引數和引數之間相互獨立為假設前提,思路簡單,而實際業務場景下,引數和引數之間是可能存在關聯的,在生成方式上還有較大提升空間,後期會在當前逐條生成的能力下,引入元啟發探索演算法,如在該領域效果比較顯著的遺傳演算法或模擬退火演算法,在每生成一條測試用例時都呼叫探索演算法,以提升覆蓋率和重要覆蓋元素為目標,生成有效測試用例集,這也是智慧UT中的最重要的『智慧』場景之一,資料是揭錯之本。

3.3 程式碼生成能力

3.3.1 解決思路

程式碼生成領域目前主要有兩個重要方向:程式生成和程式碼補全。生成測試程式碼屬於程式生成方向,採用深度學習演算法生成程式碼是目前學術界當前比較重要的研究方向,已經基於一些開源的程式碼作為語料庫取得了一定的技術突破,但因存在泛化能力弱的問題,還無法在工業界落地。

在實際技術落地中,程式生成的正確性直接影響測試任務的穩定性,考慮到這一約束,本文目前採用基於語法規則和模板的生成方式來生成測試用例程式碼。語法規則和程式碼結構資料正確即可保證生成程式碼語法正確,達到生成即可編譯的目標。

具體實現方案參考如下圖所示,將上述步驟得到的程式碼結構資料和測試用例集合資料下發給程式碼生成處理模組,模組透過控制層選擇不同語言對應的生成器,再根據不同型別選擇對應的生成運算元。對於可變內容,深度遍歷程式碼結構資料的每一個函式節點、引數節點和全域性節點,針對各自節點下的程式碼資訊,獲取對應的語法適配生成運算元來生成目的碼,從而得到測試用例程式碼,再結合模板中的固定原始碼,封裝成可編譯執行的單測程式碼。這一過程可以類比編譯器結合語法樹生成目的碼的過程。

像C/C++語言,生成基於Gtest的死亡測試封裝的測試用例程式碼,測試被測函式是否非預期死亡。還可以基於當前的生成框架,便捷地擴充套件其它語法規則來生成不同語言不同形式的用例程式碼。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(程式碼生成方案示意圖)

3.4 完整demo展示

如圖所示是一個被測原始碼exlore_filter函式經過程式碼分析、用例資料生成後得到測試用例程式碼的過程樣例。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(測試用例集合demo圖)

3.5 失敗用例分析

基於上一節介紹的程式碼生成能力,可獲得可編譯的測試用例程式碼,透過編譯適配模組生成編譯命令,執行編譯後即可得到可執行的測試程式。如何保證執行測試程式後快速獲取失敗資訊,降低人介入的分析成本,是本節重點介紹的內容。

整個分析過程中可能存在的問題如下:

  • 可讀性差:測試用例失敗後,其堆疊/crash不完整,或者無用資訊太多。像c/c++語言的gtest死亡測試,用例crash後是無堆疊資訊列印出來的,常規方式是透過gdb來獲取堆疊內容,當堆疊檔案過大超過3G時,讀取速度會很慢。

  • 重複的堆疊/crash。

  • 同一函式同一程式碼行問題重複,主要是不同用例之間命中的問題重複 例如,如下場景的find函式,在輸入用例為{arr=nullptr,len=1}和{arr=nullptr,len=2}時都會命中sum+=arr[0]這一行的crash。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

  • 不同程式碼行問題重複,主要是程式碼語義相近導致的問題重複 例如如下場景的兩個程式片段A和程式片段B,_dest是類Action的成員變數,會在程式執行的其它階段被賦值。add_to_dest和get_from_dest分別crash在write_dest->write和read_dest->read行。其程式碼行內容是不同的,但crash的語義是相同的,都是使用了空指標_dest導致程式crash。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(被測程式碼片段A)

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(被測程式碼片段B)

3)定位成本高:對新人或不熟悉堆疊檔案的人來說,測試用例程式碼、CR以及堆疊資訊都完備的情況下,依然存在跟進排查無頭緒的問題。

4)修復標準不統一:哪些問題一定要修復、哪些問題可以忽略掉,不同業務線缺少統一的標準。

本文透過堆疊內容儲存、堆疊內容分析、去重、失敗原因預測以及失敗問題分級等手段來解決上述問題,解決思路如圖所示,每個階段細節較多,本文不重點展開介紹。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(堆疊分析過程)

3.6 技術架構

面向業務落地需要考慮如何將工具的能力發揮到合適的階段,做到恰到好處,結合研發開發習慣,我們考慮瞭如下兩方面因素:

  • 存量問題需要修復週期:業務模組直接掃描,會因歷史遺留問題過多而產生較大修覆成本,需要一定的時間來消化。

  • 代時只需要關注變更影響面:在變更流水線上掃描全量程式碼,對全量程式碼生成用例會造成資源浪費以及執行效率低的問題。

基於上述考慮,我們將落地方式劃分為兩種模式:存量和增量。

  • 存量:新接入模組建議先跑全量,掃描存量問題,讓研發團隊出統一修復負責人,進行統一修復,消除存量隱患。也可以在daily任務或全量回歸任務中跑存量掃描模式。

  • 增量:是指只針對變更程式碼,透過白盒分析手段,分析出其影響的程式碼範圍,如直接影響(改動函式)、間接影響(未改動但邏輯上會有影響),只對影響範圍內的函式進行測試。在修改程式碼提交後,可觸發流水線跑增量模式的任務。

這裡還可以引入風險考量,評估出函式修改內容是否需要測試,剔除掉無風險函式。

基於上述思路,將程式碼分析、用例資料生成和程式碼生成能力整合到 如圖所示的技術架構中,和百度內部策略中臺、資料中臺、視覺化平臺等能力結合,貫徹 “測試準備、測試執行、測試分析到問題定位” 這四個維度,完成基於單測生成的異常召回工具的建設和落地。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(落地架構圖)

部分任務結果如圖所示,研發人員本地開發提交程式碼後自動觸發流水線繫結的智慧UT測試任務,透過報告可檢視到crash問題詳情,包括失敗原因、堆疊內容等。

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF

(任務執行展示樣例圖)


四、效果

長文:基於程式碼的測試生成技術在召回異常問題中的應用實踐 | IDCF



4.1 工程效果

探索出基於單測解決異常問題的通用方案,已在C/C++語言上落地實踐,累計生成千萬餘行測試程式碼,其它語言進行中。

  • 高覆蓋:冷啟函式覆蓋50%+,分支覆蓋20%+。

  • 低資源:機器資源消耗同系統級測試相比可忽略。

  • 低人耗:自動適配UT及測試程式碼編譯能力,無需人工搭建單測框架和維護。

4.2 業務效果

  • 落地:覆蓋140+重點後端模組、lib庫。

  • 存量召回:召回存量問題900餘例。

  • 增量召回:增量召回問題200餘例。

參考資料

  1. cppcheck:

  2. Fuzzing:%E6%A8%A1%E7%B3%8A%E6%B5%8B%E8%AF%95/2848962?fr=aladdin

  3. z3:

  4. all-pairs_testing:

  5. 死亡測試:

  6. traceback:實現思路參考

  7. address sanitizer:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31558019/viewspace-2771470/,如需轉載,請註明出處,否則將追究法律責任。

相關文章