Wings-讓單元測試智慧全自動生成
前言
單元測試是保證軟體質量非常有效的手段,無論是從測試理論早期介入測試的理念來看或是從單元測試不受UI影響可以高速批量驗證的特性,所以業界所倡導的測試驅動開發,這個裡面提到的測試驅動更多的就是指單元測試驅動。但一般開發團隊還是很少的系統化的執行單元測試,針對應用軟體的測試更多是由專業測試團隊來執行黑盒測試。單元測試的最大的難點不在於無法確定輸入輸出,這畢竟是模組開發階段就已經定好的,而在於單元測試用例的編寫會耗費開發人員大量的工時,按照相關統計單元測試用例的時間甚至會遠超過功能本身開發的時間。以下是幾個最常見的開發不寫單元測試的理由:
●需求總是無窮盡的,還有下階段功能需求要實現,沒空補單元
●要補的單元測試太多,無從下手,主觀上抗拒。
●單元測試編寫難度大。一方面原因可能是功能函式實現上不夠合理,另一方面是沒有(或者不知道)好用的單元測試框架和mock框架。
●單元測試不算入工作量內。
其次,功能需求還不穩定,寫單元測試的價效比不高。換句話說,萬一明天需求一變,那不光功能程式碼廢了,單元測試也廢了。如果不寫單元測試,那這部分工夫就不會白費。
上述幾點其實分析根本原因是單元測試編寫太耗時,最終導致測試驅動的發動機失去了動力,致使測試驅動開發的美好願景在現實場景熄火,因為構建這個驅動用的發動機實在是難度和成本太大了。 市場上的各種“x”Unit,單元測試框架僅僅解決了生成測試驅動的外框,沒有任何基於深度程式理解的用例邏輯和資料的產生能力。因此在各種開發相關場景中都讓開發人員產生牴觸情緒。Wings的釋出(目前針對C語言)則解決了這個困擾程式設計師的一個最大的難題,同時也有可能從根本上改變單元測試的現狀,充分的、高效率的單元測試將有效緩解基於海量人力的系統級黑盒測試以及自動化測試的壓力。
制約測試用例採用程式自動生成,最關鍵的底層技術是複雜的引數解析技術。即:能夠在編譯器層面對於任意複雜的型別,任意定義巢狀層級的遞迴解析。如果沒有這個關鍵技術的突破,那麼測試用例自動生成系統要麼無法商用,要麼將以極低的效率來演化、產生合規的測試資料。例如著名的模糊測試工具American Fuzzy Lop,它並不能夠識別使用者的程式所需要的結構型別,需要從最外層進行基於搜尋演算法的演化。程式的特性是介面層面的輸入和內部某個模組的資料要求距離很遠,外部資料通常是經過層層複雜轉換才可以成為內部模組所需要的資料結構型別,因此從外層探索所需要的計算量和時間將是難以想象的。基於American Fuzzy Lop,為了能夠生成一個合法的SQL 語句,讓程式內部模組能夠通過外圍資料校驗需要探索時間以天數計,遠非分鐘或者小時可以生成。另外一個制約性條件是:每個程式能夠接手的輸入都是經過精心結構編制、含有大量規則的資料,而這些資料通過隨機+探索的方式生成是非常不現實和極其耗時的。所以,從黑盒以及最外層輸入產生自動產生用例是不可行的。
如果從軟體內部結構分析產生用例驅動,就需要對軟體的編譯結構進行深度理解。可行的測試用例生成系統,應該是基於程式的中間(關鍵入口)作為測試切入最為合適。這些模組的輸入,已經將模糊的輸入轉化為高度結構化的引數。只要能夠識別這些複雜結構,將複雜資料型別一步步降解為簡單資料型別,同時完成引數構造,就可以自動完成驅動用例的生成。
基於模組的測試,可以劃歸為傳統的單元測試,它是將缺陷發現並遏制在研發階段最好的方法。但受限於單元測試需要開發大量的驅動程式,在行業內的推廣和應用受到了極大的限制。當然單元測試也可以在系統整合完畢後執行,避免構建虛擬的樁程式。 星雲測試日前全球首發的Wings產品,是一個智慧的、全自動的單元測試用例生成系統,研究並解決了如下難點,現分享給大家。
(1) 程式引數深度分析問題
Wings通過編譯器底層技術,將輸入的原始檔,按照函式為單位,形成模組物件。物件中包含函式的輸入引數,返回值型別等資訊,供驅動函式模組和測試用例模組使用。每個檔案作為一個單元,針對其中的每個函式的每個引數進行深度解析,對於巢狀型別,複雜型別等都可以實現精確的解析和分解,將複雜型別逐層講解為基礎資料型別,併產生引數結構的描述檔案(PSD)。
(2) 函式驅動自動生成模組
依據PSD檔案的格式資訊,自動生成被測源程式的所有驅動函式,單元測試過程不再依賴開發人員手動編寫測試函式,只需將生成的驅動函式和被測原始檔一起編譯,即可執行測試並檢視測試結果。測試驅動自動生成程式基於PSD描述,全自動構建驅動被測程式執行的所有引數,必須的全域性變數,並可根據複雜變數的層級結構產生結構化的測試驅動程式,可以節省大量的單元測試用例的編寫時間。
(3) 測試資料自動生成與管理
用於自動生成測試資料,測試資料與被測函式提取的資訊相互對應,資料以一定的層次邏輯關係儲存在json檔案中。資料和經過分解和展開後的資料型別是一一對應的。這些資料使用者可以根據業務要求隨意邊際,並且用json檔案進行結構化,層次化展示,非常的清晰。其中的測試資料包括全域性變數值、被測函式呼叫時的引數值。 Wings提供了一種自動生成驅動函式的單元測試方法,其中主要包含以下幾個步驟:
(被測程式資訊提取→ 驅動程式的自動生成→ 測試資料自動生成)
1 被測程式資訊提取
通過對源程式的掃描提取出函式的結構資訊,使使用者不需要關心程式的結構資訊,而被測程式的結構資訊,主要包含程式中的全域性變數以及函式資訊,而函式資訊主要包括函式的引數個數,引數型別以及返回值型別。而全域性變數以及引數,最主要的提取出其中的符號資訊,以及型別資訊,針對一些複雜的型別,通過層層進行解析為基本資料型別,完成全域性變數以及函式引數的構造。
變數的型別一般大致分為基本型別、構造型別、指標型別及空型別。Wings通過底層編譯技術,針對不同的變數型別,進行不同的處理方式。
(1)基本型別,例如unsigned int u_int = 20等基本型別,Wings將解析出變數的名稱為u_int,資料型別為unsigned int。
(2) 構造型別,構造型別大致分為陣列,結構體,共用體,列舉型別。
-
陣列型別,例如int array[2][3],陣列名稱為array,型別為int以及二維陣列的長度,行為2,列為3。
-
結構體型別,針對結構體為陣列,結構體連結串列等,進行不同的標記劃分。
(3) 指標型別,例如int **ptr = 0;,解析出指標為int型別的2級指標。
(4) 空型別,解析出型別為NULL。
(5) 系統型別,例如File、size_t等,標記為系統型別,不在對其往下進行分析,會新增到模板中,由使用者進行賦值操作。
(6) 函式指標型別,分析出函式的返回值型別、引數型別以及引數個數 針對被測源程式的每個編譯單元,將解析到的函式資訊,儲存在對應的PSD結構中,針對以下原始碼例項進行說明:
typedef struct my_structone
{
//基本型別
int i_int;
//陣列型別
int array_one[2];
int array_two[3][4];
//指標型別
int *point_one;
int **point_two;
//空型別
void *point;
//位域型別
unsigned int w : 1;
//函式指標是指向函式的指標變數,即本質是一個指標變數
int(*functionPtr)(int, int);
union
{
int a;
char b;
long long c;
}Dem;
enum DAY
{
MON = 1, TUE, WED = 200, THU, FRI = 100, SAT, SUN
}dy;
}myy_structone;
typedef struct my_struct
{
//結構體包含結構體
myy_structone *structone;
//結構體中包含系統標頭檔案的型別
FILE file;
struct my_struct *next;
}myy_struct;
//結構體作為函式引數
void StructTypeTest1(myy_struct m_struct);
void StructTypeTest2(myy_struct *mm_struct);
void StructTypeTest3(myy_struct mm_struct[2]);
void StructTypeTest4(myy_struct mm_struct[2][3]);
複製程式碼
以上程式中,void StructTypeTest3(myy_struct mm_struct[2])儲存的PSD結構如下:
<StructTypeTest3 parmType0="myy_struct [2]" parmNum="1">
<mm_struct baseType1="ArrayType" RowSize="2" type="StructureOrClassType" name="my_struct">
<structone baseType1="PointerType" type="StructureOrClassType" name="my_structone">
<i_int baseType1="BuiltinType" type="ZOA_INT" />
<array_one baseType1="ArrayType" RowSize="2" type="ZOA_INT" />
<array_two baseType1="ArrayType" RowSize="3" baseType2="ArrayType" ColumnSize="4" type="ZOA_INT" />
<point_one baseType1="PointerType" type="ZOA_INT" />
<point_two baseType1="PointerType" baseType2="PointerType" type="ZOA_INT" />
<point baseType1="PointerType" type="ZOA_VOID" />
<w baseType1="BuiltinType" type="ZOA_UINT" bitfield="1" />
<functionPtr baseType1="FunctionPointType" type="ZOA_FUNC" returnType="int" parmType0="int" parmType1="int" parmNum="2" />
<Dem baseType1="UnionType" type="ZOA_UNION" name="NULL">
<a baseType1="BuiltinType" type="ZOA_INT" />
<b baseType1="BuiltinType" type="ZOA_CHAR_S" />
<c baseType1="BuiltinType" type="ZOA_LONGLONG" />
</Dem>
<dy baseType1="EnumType" type="ZOA_ENUM" name="DAY">
<MON type="ZOA_INT" value="1" />
<TUE type="ZOA_INT" value="2" />
<WED type="ZOA_INT" value="200" />
<THU type="ZOA_INT" value="201" />
<FRI type="ZOA_INT" value="100" />
<SAT type="ZOA_INT" value="101" />
<SUN type="ZOA_INT" value="102" />
</dy>
</structone>
<file baseType1="StructureOrClassType" type="StructureOrClassType" name="_iobuf" SystemVar="_iobuf" />
<next NodeType="LinkNode" baseType1="PointerType" type="StructureOrClassType" name="my_struct" />
</mm_struct>
<g_int globalType="globalVar" />
<returnType returnType="void" />
</StructTypeTest3>
複製程式碼
其中PSD檔案各節點代表的意義如下:
-
StructTypeTest3代表函式名,parmType0代表引數型別,parmNum代表引數個數
-
mm_struct代表函式引數的符號,baseType1代表型別的分類(基本資料型別、構造型別、指標型別、空型別),type代表具體的型別,包括int,char,short,long,double,float,bool,以及這些型別的unsigned型別等基礎的型別,還有一些特殊的型別諸如: ZOA_FUN型別表示函式型別,StructureOrClassType表示結構體型別,等等,name代表結構體、聯合體、列舉型別的名稱
-
i_int代表基本型別,基本型別作為最小的賦值單位
-
array_one代表陣列型別,RowSize代表陣列的長度,陣列可以劃分為一維陣列,二維陣列等
-
point代表指標型別,指標分為一級指標、二級指標等,一般指標當做函式引數作為陣列使用,因此,針對基本型別的指標,採用動態分配陣列的方式進行賦值,使用者可依據需要,修改對應的值檔案。
-
w代表位域型別,bitfileld代表所佔位數
-
functionPtr代表函式指標型別,分別分析出引數型別、引數個數、返回值資訊
-
Dem代表聯合體型別
-
dy代表列舉型別,value代表列舉型別的取值
-
file代表結構體型別,SystemVar代表此變數屬於系統標頭檔案中的變數,針對此種型別的變數,Wings通過新增模板變數的方式,新增在模板庫中,使用者可依據具體需要進行特殊賦值。例如File型別的,處理方式為:
/* 系統內建型別,特殊處理或者模板處理 */
char * fname = "E:/spacial.txt";
FILE * file = fopen(fname,"r");
_st.file = _file;
複製程式碼
使用者也可自行新增賦值方式。針對系統型別,Wings可以和普通使用者自定義型別進行區分,當解析到系統內建型別的時候就可以停止向下進行遞迴分析。
-
g_int代表全域性變數,globalType代表全域性
-
next代表連結串列結構體,NodeType代表此結構為連結串列
-
returnType代表函式的返回值型別。
2 驅動程式的自動生成 在上文中,針對全域性變數和函式的結構資訊,進行了分析和提取,以下將利用提取到儲存在PSD中的資訊,完成被測源程式的驅動框架整體生成。 生成主要分為以下幾個方面:
-
全域性變數的宣告
-
函式引數的賦值操作,針對函式引數的個數,依次賦值操作
-
全域性變數的賦值,針對分析得到函式使用的全域性變數的個數,依次進行賦值操作
-
原函式的呼叫
一些需要注意點如下:
-
驅動生成過程中,針對一些特殊函式,例如main函式,static函式等,因為外部無法訪問到,驅動生成暫時不做處理。
-
針對每個被測原始檔,生成對應的一個驅動檔案。
-
驅動控制包含在Driver_main.cpp中,可以通過巨集自動配置函式的測試次數
由以上源程式,生成的驅動函式如下:
-
所有變數的命名為在原變數的名稱前,新增_
-
通過獲取生成對應的測試資料,對變數依次進行賦值操作
-
針對系統內建引數,以及使用者比較特殊的引數,通過模板方式統一配置賦值方式。
-
對被測函式進行引數賦值與呼叫。
3 測試資料自動生成 測試用例的自動生成,利用提取到儲存在PSD中的函式資訊,進行測試用例資料的生成,以下是圖三中PSD格式生成的一組資料,每組資料儲存為JSON格式,更容易看到資料的層次關係。
"StructTypeTest30" : {
"g_int" : 11624,
"mm_struct" : [
{
"file" : "NULL",
"next" : "NULL",
"structone" : {
"Dem" : {
"a" : 20888,
"b" : "A",
"c" : 19456
},
"array_one" : [ 24441, 12872 ],
"array_two" : [
[ 18675, 30300, 32216, 19566 ],
[ 13566, 13319, 11179, 18867 ],
[ 30514, 21664, 21641, 28262 ]
],
"dy" : 101,
"functionPtr" : "NULL",
"i_int" : 18271,
"point_one" : [ 28024, 32245, 2129 ],
"point_two" : [
[ 18165, 32335, 6429 ],
[ 30225, 18252, 2764 ],
[ 3177, 3622, 29789 ]
],
"w" : 16862
}
},
{
"file" : "NULL",
"next" : "NULL",
"structone" : {
"Dem" : {
"a" : 2651,
"b" : "7",
"c" : 12159
},
"array_one" : [ 1274, 24318 ],
"array_two" : [
[ 27944, 1208, 29647, 20840 ],
[ 4972, 27297, 17456, 13614 ],
[ 22441, 1160, 8940, 29420 ]
],
"dy" : 200,
"functionPtr" : "NULL",
"i_int" : 15434,
"point_one" : [ 29394, 3868, 25406 ],
"point_two" : [
[ 13575, 14736, 20728 ],
[ 9132, 2297, 2113 ],
[ 26252, 14896, 10985 ]
],
"w" : 12354
複製程式碼
針對每個編譯單元,預設生成一組所有函式的對應的測試資料檔案,值生成可以通過配置次數進行修改。 4 Mysql程式測試結果展示 如何完成驅動框架的生成,下面針對開源程式MySQL完整的生成過程,進行詳細說明。 以下是Wings測試Mysql的主介面圖:
點選檔案按鈕,設定被測源程式的工程目錄。設定完成之後,點選功能操作,功能操作主要包括引數解析、驅動生成、值檔案生成以及模板新增四個操作。分析對應生成以下幾個資料夾:
其中,引數解析模組,對應生成FunXml以及GlobalXml,分別存放提取到的每個編譯單元的函式資訊及全域性變數的資訊。 驅動生成模組,會對應生成Wings_Projects資料夾,其中存放每個編譯單元的驅動檔案 值生成模組,存放每個編譯單元的生成的測試資料。 下圖為Mysql對應載入的驅動檔案結構體資訊,左側導航樹為生成的對應驅動檔案,包含每個編譯單元的函式以及函式的引數、全域性變數的資訊。點選其中某個編譯單元,可以載入對應的驅動檔案以及對應的值檔案。
以上是Mysql的整體生成對應的驅動檔案以及值檔案,針對以下程式碼詳細說明驅動檔案。
-
針對每個編譯單元,全域性變數的引用通過extern的方式。
-
驅動函式,統一命名為Driver_XXX的方式,JSON作為獲取測試資料的方式,times代表單函式的測試次數。
-
針對每個引數的賦值操作,利用解析到的PSD儲存格式,對每層結構依次進行賦值操作。
Wings的應用非常簡單,下面是以在Visual Studio 2015中可正常編譯的Mysql 程式碼為例,生成的測試資料的統計指標,整個生成過程無需任何人工介入,僅需要制定所需要生成驅動的原始碼的路徑即可。 mysql測試資料: Mysql版本 5.5 C語言程式碼檔案個數 578個 分析所用時間(PSD生成時間) 149.099s 驅動生成所用時間 27.461s 值生成所用時間 84.974s 電腦配置說明: 作業系統 Windows7 處理器 Inter(R) Core(TM) i7-7700cpu 3.60GHz 記憶體 8.00GB 系統型別 64位
以下是使用原始碼統計工具得到的結果,多達400多萬行有效的單元測試程式碼是由Wings全自動生成的。更有意思的是:可以看到這些程式碼採用人工開發的成本高達1079個人月,成本更是達到了1079萬之多。
Wings實現了由程式自動生成程式的第一步探索,目前釋出的是第一版,有興趣的開發者直接在碼雲平臺(https://gitee.com/teststars/wings_release進行下載),商業授權提供了一個月無限功能體驗期,可以快速體驗Wings的神奇能力,Wings c語言版支援多平臺,例如visual studio、vxworks、gcc、qt等。Wings由星雲測試(www.teststar.cc)團隊設計和研發,有興趣的開發者可以通過碼雲的互動平臺與星雲測試團隊取得聯絡,貢獻自己的設計思路和產品使用反饋(凡被採納的優秀建議,星雲可以延長其免費使用期至少為三個月)。Wings具有強大的、底層的大幅度改進軟體質量的基因,未來Wings的將深度優化自動編寫的程式的可讀性(更接近優秀程式設計師的編寫水平)以及對於c++語言的支援。