軟件質量保障
所寫即所思|一個阿里質量人對測試的所感所悟。
1. 介紹
有句話說:證實容易,證偽難。正如測試一樣,證明缺陷存在容易,但證明不存在缺陷難。而變異測試顛覆了這一原則,如果我們知道存在缺陷,那么我們的測試結果會如何反映測試的質量呢?
隨著工程師越來越多地采用更自動化的軟件驗證方法,以及在不斷縮短的發布周期中對更高品質的軟件輸出的需求日益增長,變異測試幫助我們退一步評估,我們是否真的應該對我們的測試充滿如此信心。
Mutation testing is the process of heuristically determining semantics of your program that are not covered by tests.
變異測試是通過啟發式方法確定程序中未被測試覆蓋的語義的過程。——Markus Schirp
在多數軟件測試方法中,很難預判能否在測試過程中發現缺陷,往往直到這些缺陷在后續的測試環節被發現,甚至是更糟的情況下,在生產環境中出現時才會被注意到。這對于每位測試經理來說都頗為熟悉,因為他們需要根據生產環境發布后獲得的經驗來反饋并改進測試流程。無疑,是否存在一種更佳的方法來揭示測試覆蓋中的問題呢?

變異測試作為一種測試技術,其歷史可追溯至1971年,近年來越來越受到重視,現已發展出十幾種相關測試工具,并廣泛應用于各種軟件環境。可用工具的數量自1981年的不足5種顯著增長,到2013年已超過40種。
變異測試處在依賴系統期望行為正式規范的測試技術灰色地帶,與之并列的還有模糊測試、變形測試以及探索性測試。從根本上講,變異測試是一種實踐,即對軟件(所謂的“變異體”)的多個版本執行一組測試。每個待測軟件版本都有意且通過編程方式注入了不同的缺陷。每一次測試迭代都是基于啟發式方法在軟件的一個微小差異版本上進行的——這些規則對應于常見缺陷——注入不同的缺陷。這些版本被稱為“變異體”,意指它們是小規模的變異。測試的目的通常是確定哪些缺陷能被測試程序發現,從而導致執行失敗。
之所以將軟件的這些人造缺陷版本稱為“變異體”,是因為它們的變異方式類似于人類基因在自然進化過程中的變異,這與人工智能中使用遺傳算法解決搜索和優化問題有相似之處。
變異測試的好處可能非常明顯,它能為以下方面提供極為有用的洞察:
? 自動化測試的質量和覆蓋范圍,尤其是斷言和驗證的覆蓋充分度情況
? 特定測試設計技術的成功實施
? 代碼不同模塊的復雜度和可維護性
? 通過測試追溯組件到整體業務功能的能力
它可以作為一種方法來改進現有測試集,以確保有足夠的測試驗證或優先針對代碼的特定模塊進行測試。最終,它提供了一組關于質量的真實信息,這些信息通過其他方式是無法獲得的。
2. 自動化測試
從概念上講,變異測試并不局限于自動化測試;從原則上講,它是一種可以應用于手工測試的方法。然而,在大多數情況下與運行數千次手工測試相關的成本無法與收益相提并論。

自動化測試的核心挑戰之一是確保斷言和驗證點的同步。測試工程師理應感知更多的上下文知識,并利用專家經驗來確定測試用例的預期并進行斷言。與手工測試不同,自動化測試集的本質是,它們只會驗證預期存在的缺陷——換句話說,它只會對預期內的返回做斷言。舉個最極端的例子,你可以創建一個自動化測試,啟動要測試的軟件,只需驗證它可以成功運行,而不對程序輸出內容做任何斷言。這將是一個非常易于維護的測試,但也是一個價值非常低的測試,因為它除了告訴我們程序正常運行外,沒有告訴我們其他內容。如果使用變異測試來評估這種假設軟件和測試,得分將非常低,因為軟件的變異版本只有在注入的缺陷導致軟件完全運行失敗時才會使測試報錯。變異測試不能解決測試可維護性的問題,但它確實能對單個測試的實際價值提供一定的見解。這一點也很重要,因為即使在一個自動化程度很高的環境中,人們也往往不會執行全面的組合測試,因為它被認為是一種不切實際的資源使用方式。任何測試設計技術都存在一個風險,即測試用例爆炸,也就是說,當你專注于一種特定的測試設計技術時,測試的數量會急劇增加。
3. 代碼覆蓋率和變異運算符
變異測試是一種黑盒測試技術,測試人員做測試設計和執行不需要了解代碼的內部執行邏輯,但是變異植入人員必須非常熟悉了解代碼。事實上,生成的變異體與底層代碼的語言和結構密不可分,真正的缺陷也是如此。當變異植入人員將缺陷注入代碼,這里變異體通常可以由編寫代碼的開發工程師自定義,包括:
?從代碼中刪除語句
?從代碼中插入語句
?更改代碼中的條件
?替換變量
讓我們回顧一些基本的編程概念以及它們與缺陷、代碼覆蓋率和變異測試的聯系。在大多數編程語言中,可執行語句表示要執行的操作,例如將一個值(例如true或false)分配給一個變量。ISTQB術語表將此定義為:“當編譯時,該語句被編譯成目標代碼,并且在程序運行時將按順序執行,并且可能對數據執行操作。”例如,在下面的代碼中,所有不以IF/ELSE開頭的代碼都是語句:
allowEntrance = false
if(customerHasMembershipCard or customerHasAccessCard):
allowEntrance = true
price = 0
else:
if(weekend):
allowEntrance = true
price = 10
可執行測試集覆蓋語句的程度通常被稱為語句覆蓋率,語句覆蓋率是測試集執行的語句的百分比。
語句覆蓋率=執行語句數/總語句數
要達到上述代碼的完整語句覆蓋,你需要兩個測試,因為要執行每個語句,需要通過代碼中的兩個互斥路徑。另一種覆蓋方法是覆蓋每個分支或決策,基本上,只要包含條件語句(如if、for、while),就要確保條件語句的兩個結果都被評估。要達到上述示例的完整分支覆蓋,你需要一個額外的測試,以覆蓋weekend變量是否為真或假。

最后,條件覆蓋率是一種代碼覆蓋率度量標準,它衡量的是每個單個條件是否已被評估為真或假。這可以計算為:

因此,再次以上面的例子為例,確保測試覆蓋了會員身份和訪問卡場景,為我們不斷增多的測試集添加另一個測試。僅使用代碼覆蓋率指標來衡量自動化測試質量的問題在于,這些指標沒有一個可以評估我的測試是否實際上檢查了客戶是否被允許訪問,或者軟件計算了多少費用。這些狀態和變量的驗證不包括在指標中。在自動化測試中測量代碼覆蓋率固然很好,但這只是畫面的一部分。代碼覆蓋率只告訴你已經執行的邏輯和分支,它不能真正衡量你的測試是否獲得了大量的功能覆蓋數據,也不能告訴你你的測試是否有效地檢測到了缺陷。驗證系統響應(有效比較實際結果與預期結果)是實現自動化測試的關鍵部分,直接檢查一個變量是否合理是很容易的;然而,隨著接口變得越來越復雜,圍繞驗證的設計主觀性也在增加。變異操作引擎將變異操作符應用于代碼,它們支持的操作符各不相同,實際上用戶通常可以配置如何應用這些操作符。例如,著名的Mothra研究和支持工具使用了表1中所示的Fortran中的操作符。這些操作符有些過時,因為操作符隨著面向對象技術的發展而發展。

當然,潛在操作符的數量及其導致的軟件代碼變異是巨大的,任何實現都不可能是窮盡的。通過一個變異操作符運行測試軟件可能會導致代碼中的許多變化。例如,基于規則的操作符應用于此代碼:
allowEntrance = false
if (customerHasMembershipCard or customerHasAccessCard):
allowEntrance = true
price = 0
else:
if(weekend):
allowEntrance = true
price = 10
在這個例子中,紅色項目將被更改。第一個將被初始化為true,第二個將把or改為and,第三個和第四個將通過添加not來否定布爾值。因此,將被編譯出四個測試系統的版本,并且變異測試例程應該針對每個版本運行所有自動測試。為了成功檢測每個變異體,測試集需要具有以下特征:
?測試如果客戶沒有兩張卡,并且不是weekend,則不允許客戶進入。這將測試變異1中未初始化變量的情況。
?測試如果客戶有一張卡,但沒有另一張卡(完整條件覆蓋也需要測試這種情況),然后驗證客戶被允許進入。
?測試如果客戶在weekend沒有卡是否被允許進入。
?替換變量?
正如你所看到的,盡管為達到代碼覆蓋率而構建的測試集會通過代碼執行類似的路徑,但變異測試指標允許對測試應執行的驗證進行更具體的描述。畢竟大多軟件缺陷是在編碼過程中引入的。例如,常見的“下標偏移”缺陷,即程序員指示循環迭代一次或多次,或者太少——或者誤算了邊界條件——可以直接通過邊界值分析和等價劃分等測試設計技術來解決。同樣,這種缺陷通常是由變異測試操作員注入的。變異測試過程可以概括如下:
1. 通過插入缺陷來創建變異體。
2. 變異體創建后,選擇并執行測試。
3. 如果變異體執行測試時測試失敗,則變異體將被“殺死”。
4. 如果變異體測試的結果與基礎軟件相同,則變異體“存活”。
5. 可以添加新的測試,修改現有的測試或重構代碼,以增加“殺死”的變異體數量。一些變異體無法被檢測到,因為它們會產生與測試的原始軟件等價的輸出,這些被稱為“等價變異體”。一旦整個過程執行完畢,就可以計算變異體得分。這是殺死的變異體與變異體總數的比率。這個分數越接近1,測試集和軟件的質量就越高。
變異得分=殺死的變異體數量/總變異體數量
4. 變異測試的挑戰和策略
進行變異測試所需的成本比較高,而且需要大量花費時間來檢查和修復發現的問題,需要考慮成本如下:
?生成變異體的編譯時間成本
?在變異體上運行測試的運行時間成本
?分析結果的人力成本。
編譯時間正如前面提到的,變異測試的一個主要問題是執行成本。變異體的數量是代碼行數和數據對象數之積,但通常情況下,生成的變異體的數量通常是代碼行數的平方。一些策略已經嘗試減少執行量:
-
抽樣——在軟件的邏輯區域和相關測試中僅執行變異體的隨機樣本。
-
聚類——使用無監督機器學習算法(例如,K-means)來選擇變異體。
-
選擇性測試——減少變異體操作符的數量,即用于注入缺陷的啟發式方法,可以減少變異體的數量60%。
-
高階變異——一階變異體是那些只注入一個缺陷的變異體;二階變異體是在多次變異迭代中注入多個缺陷的變異體。高階變異體更難殺死,而僅關注二階變異體已被證明可以減少工作量,而不會減少覆蓋率。
-
增量變異——只對變更代碼進行變異測試,而不是測試中的整個代碼庫。
我們知道,缺陷具有集聚性,也許一個簡單的策略,將技術應用于有限、復雜、高風險和充滿缺陷的功能區域,可以提供成本與收益的適當平衡。相反,代碼中有些語句我們不必擔心。例如,從變異中排除所有日志語句可能是適當的,并導致更少的變異體進行測試。另一種減少所需時間和資源的方法是直接與編譯器集成。早期的變異測試方法單獨編譯每個變異體;然而,更現代的方法編譯一次,然后對中間形式(如字節碼)進行變異。這在編譯性能方面具有顯著優勢,但在評估每個變異體的執行時間方面沒有優勢。
運行時間運行變異測試周期的運行成本可能是巨大的。當然,在大多數情況下,執行可以水平擴展,因為測試可以在多臺機器上運行,或者可以通過使用更強大的機器進行垂直擴展。為了應用水平可擴展性,有必要在選擇適當的變異測試工具時考慮這一點,并支持此類方法。還需要考慮提高測試運行時間的經典測試自動化技術。當測試運行一次時,內置到測試中的硬編碼等待可能不會引起注意,但當擴展到數百或數千次執行時,就會成為一項重要的成本。在嘗試引入變異測試之前,應優化自動化測試以提高性能。
分析兩個相關的挑戰是Oracle問題以及減少等價變異的問題。Oracle問題遠非變異測試所獨有,它適用于任何難以確定測試是否預期的測試領域。當無法殺死變異體時,就會發生這種情況,因為需要在測試中實現的斷言太難實現。當軟件的確定性較低,并且難以理解檢測不到變異是否實際上是有意義時,也會發生這種情況。那么最大的挑戰是什么:
最大的問題是測試oracle問題。對于小型項目來說,這不是什么大問題。然而,對于大型項目來說,變異會產生數千個變異體,其中數百個是有效的。目前還不清楚開發工程師應該如何處理它們——人工review這些顯然是不切實際的。
減少等價變異的問題也很重要,也就是說,變異不會導致輸出可觀察變化。這可能是因為未執行過的僵尸代碼;變異只改變軟件的速度;或者變異只改變內部使用的數據,不影響最終輸出。
讓我們看一個等價的變異:
def foo(i): return i + 1 + 0
一個潛在的等價變異的例子是刪除“+ 0”。雖然這會改變正在測試的軟件,但它不會改變輸出,因為向一個數字加零沒有任何實際效果。這正是關鍵所在,代碼無關緊要,應該刪除。
5. 變異測試工具
變異測試工具的種類繁多,這在一定程度上是因為它們與實現中使用的編程語言有著內在的聯系。你通常不能使用為 C++ 設計的工具來編寫 Java。選擇一個支持底層技術棧、支持讓你配置所需的變異啟發式方法、與你的開發環境集成并支持并行和分布式執行的工具至關重要。使用這些工具可以簡單到在你的構建配置中注入依賴項。選擇和評估適合你的工具實際上比傳統工具選擇更重要。一個原因是這些工具將帶有不同的內置默認操作符和策略。正如前面提到的,這些操作符和策略有效地決定了技術方法,并直接影響結果。該工具還可以確定如何擴展運行時執行,而選擇一個支持有限的工具可能會導致不切實際的時間表。
然而,你需要考慮的不僅僅是變異工具:
作為一個實施者,我看到的更大的問題應該是如何無縫集成。如何將變異集成到現有的基礎設施中:各種構建系統、測試框架、CI 管道、IDE 等
對于測試周期而言,還有很多工具。例如,Pitest,一個Java變異引擎;Cucumber插件允許你與Cucumber行為驅動開發集成。同樣,SonarQube,一個流行的代碼質量監控集,有插件允許你顯示變異測試運行的詳細結果。一些變異測試引擎還提供IDE插件,加速反饋循環,并允許你在代碼上下文中查看結果。
6. 總結一下
本文主要是希望開闊大家的視野,以另一種視角來看測試覆蓋率。雖然變異測試僅頻繁應用在單元測試層面,但這些概念可以應用于整個軟件質量保證實踐。它不僅是一種評估自動化測試價值的有效方法,而且是一種了解代碼復雜性的方法,以及定量了解代碼質量和測試覆蓋率的方法。這些概念也可以應用于代碼、輸入數據、環境和其他技術。
- END -
下方掃碼關注?軟件質量保障,與質量君一起學習成長、共同進步,做一個職場最貴Tester!
關注「軟件質量保障」微信公眾號

好文推薦
往期推薦
聊聊工作中的自我管理和向上管理
經驗分享|測試工程師轉型測試開發歷程
聊聊UI自動化的PageObject設計模式
細讀《阿里測試之道》