資安

阿里研究員:軟件測試中的18個難題

image.png
十多年前我在上一家公司的時候看到過內部有個網站有一個Hard Problems in Test的列表,上面大概有三四十個問題的樣子,是各個部門的測試同學提供的。但可惜後來那個list失傳了,我很後悔自己當時沒有保存一份。後來很多次我都想要找到那份list,因為上面列的那些問題指出了測試專業在自身專業性上的巨大發展空間。那份list上的問題讓當時的我相信,軟件測試這件事情本身的難度一點都不亞於軟件開發,甚至可能更難一點。

如果今天要重建這麼一份Hard Problems in Test列表,下面這些問題是我會加到這份列表上的[1]。

一 測試充分度(Test Sufficiency)

如何回答“測夠了嗎“(包括測新和測舊)。代碼覆蓋率是衡量測試充分性的起點,但遠遠不是終點。要回答”測夠了嗎“,至少還要考慮是否測了所有的場景、所有的狀態、所有的狀態轉移路徑、所有的事件序列、所有可能的配置、所有可能的數據等等等等。即便如此,我們可能還是無法100%確信我們已經測夠了。可能我們最終只能做到非常趨近於測夠了[2]。

二 測試有效性(Test Effectiveness)

如何評價一組測試用例的發現bug的能力。有效性(發現bug的能力)和充分性(測夠了沒有)是兩個正交的屬性。評價測試用例有效性可以通過正向的分析進行,例如,分析測試用例是否校驗了所有在測試過程中SUT落庫的數據。更具有通用性的做法是變異測試(Mutation Testing),即在被測代碼裡注入不同的“人造bug”,統計多少能被測試用例感知到。目前變異測試我們已經有工程化規模化的落地了,後續的工作重點有:1)如何防止鈍化(或曰“殺蟲劑效應”),2)不但對被測代碼進行注入,還能對配置、數據等進行更全面的注入。

三 測試用例瘦身

以前廣告行業有句話:我知道廣告費有一半是浪費掉的,但不知道哪一半是浪費掉的[3]。

軟件測試也有類似的困惑:那麼多用例,要花那麼多時間去跑,我知道這裡面有很多時間是浪費掉的,但我不知道哪些時間是浪費掉的。浪費的形式包括:

  • 冗餘步驟:有些是浪費在一些重複的步驟上,每個用例都要去做一些類似的數據準備,每個用例都要去執行一些中間過程(這樣才能推進到下一步)。
  • 等價類:一個支付場景,我要不要在所有的國家、所有的幣種、所有的商戶、所有的支付渠道和卡組的排列組合都測一遍?這麼測,代價太高。不這麼測,我擔心可能某個特定商戶在某個特定國家有個特定邏輯我就漏掉了。對於具體的業務,還可以進行人肉分析。有沒有更通用的、而且比較完備和可靠的等價類分析的技術手段?
  • 我有N個用例,我猜這N個用例裡面可能存在M個用例,即使刪掉這M個用例,剩下的N-M個用例的效果和之前N個用例的效果一樣。如何識別是否存在這樣的M個用例、如果存在的話是哪M個。

我參加過內部一場質量線晉升到P9的評審,當時有個評委問了那位同學一個問題:“那麼多測試用例,以後你怎麼刪”。這個問題看似簡單,其實非常難。我覺得,從原理上來說,如果測試充分度和測試有效性的度量都做的非常好了、度量成本非常低了,我們是可以通過大量的不斷的嘗試來刪用例的。這是一種工程化的思路,也許還有其他的理論推導的思路。

四 測試分層

很多團隊都會糾結到底要不要做全鏈路迴歸、做到什麼程度。這個問題的核心點就是:有沒有可能、有沒有一種做法,只要把系統間的邊界約定的足夠好足夠完整,就可以做到在改動一個系統的代碼後,不需要和上下游系統進行集成測試,只要按照邊界約定驗證好自己的代碼就可以確保沒有任何regression了。

包括我在內的很多人相信那是可能的,但既無法證明,也不敢在實操中就完全不跑集成。我們也缺乏可以完全複製的成功經驗,缺乏一套完整的方法論指導開發團隊和QA團隊要怎麼做就可以達到迴歸無需集成上下游。

有時候,我覺得我現在就像是哥德堡的市民,不斷的走啊走,嘗試找出一條一次性不重複的走過那7座橋的路線。但也許就有那麼一天,有一個像歐拉那樣的人會出現在我面前,用理論證明告訴我,那是不可能的。

五 減少分析遺漏

分析遺漏是很多故障的原因。開發做系分的時候,有一個corner case沒考慮到、沒有處理。測試做測分的時候,忘記考慮某個特殊場景了。兼容性評估,評估下來沒有兼容性問題的,但結果是有的。而且很多時候,分析遺漏屬於unknown unknowns,我壓根就不知道我不知道。有沒有一套方法和技術,可以減少分析遺漏,可以把unknown unknowns轉化為knowns?

六 用例自動生成

Fuzz Test、Model Based Test、錄製回放、Traffic Bifurcation(引流)等都是自動生成用例的手段。有些已經比較成熟(例如單系統的錄製回放、引流),有些多個團隊都在探索(例如Fuzz),有些則一直沒有大規模的成功實踐(例如MBT)。我們也有過探索如何從PRD裡通過NLP來生成用例。用例自動生成中,有時候難點還不是生成test steps,難度反而是怎麼生成test oracle。Anyway,測試用例自動生成是一個非常大的領域,這個方向上未來可以做的還非常多。

七 問題自動排查

包括線上和線下。對於比較初級的問題,自動排查方案往往有兩個侷限性。首先,方案不夠通用,多多少少比較定製化。其次,比較依賴人工積累規則(說的好聽點叫“專家經驗”),主要是通過記錄和重複人肉排查的步驟來實現。然而,每個問題都不完全一樣,問題稍微一變,之前的排查步驟可能就不work了。現在有一些技術,比如調用鏈路的自動比對,對排查問題和缺陷自動定位很有幫助。

八 缺陷自動修復

阿里的Precfix、Facebook的SapFix等是目前比較知名的一些工業界的做法。但總的來說,現有的技術方案,都有這樣那樣的侷限性和不足,這個領域還在相對早期階段,後面的路還很長。

九 測試數據準備

測試用例的一個重要設計原則是:測試用例之間不應該有依賴關係,一個測試用例的執行結果不應該受到其他測試用例的執行結果(包括是否執行)的影響。基於這個原則,傳統的最佳時間是確保每個測試用例都應該是自給自足的:一個用例需要觸發的後臺處理流程應該由這個用例自己來觸發,一個測試用例需要的測試數據應該自己來準備,等等。但如果每個用例所需要用到的測試數據都是自己來從頭準備的,執行效率就比較低。怎麼既不違背“測試用例之間不應該有依賴關係”的大原則,又能減少測試數據的準備時間?

我設想的是一種更加完備的數據銀行。每個測試用例執行完後,都會把它自己產生的數據交給數據銀行,例如,一個在某個特定國家的已經通過KYC、已經綁了一張卡的會員,一筆已經支付成功的交易,一個已經完成入駐簽約流程的商戶。下一個測試用例開始的時候,會先問一下數據銀行:“我要一個滿足這樣這樣條件的商戶,你有沒有”。上個用例跑出來的那個商戶正好符合條件,數據銀行就會把商戶“借”給這個用例用。而且一旦借出,直到被歸還前,這個商戶不會被借給其他用例。

經過一段時間的運行,數據銀行能夠學習到每個測試用例需要什麼樣的數據、以及會產生什麼樣的數據。這個知識是通過學習得到的,不需要人肉去添加描述,所以也能適用於老系統的存量用例。有了這個知識,數據銀行可以實現兩個優化:

  • 一次測試執行批次開始後,數據銀行會看到這個批次中後面那些用例需要什麼樣的數據,提前先準備起來。這樣,等執行到那些用例的時候,數據銀行裡就已經有符合條件的數據準備好了。
  • 根據每個測試用例需要什麼樣的數據、以及會產生什麼樣的數據,數據銀行可以合理的編排測試用例的執行先後次序,最大化的實現測試數據的複用,減少測試數據的量和準備開銷。

測試銀行把測試數據“借”給用例的時候,可以有多種不同的模式。可以是獨佔(exclusive)的,也可以是共享的。共享的也可以指定共享讀、共享寫、還是都只讀不能寫(例如,一個商戶可以被多個用例用來測試下單支付結算場景,但這些用例都不可以去修改這個商戶本身,例如重新簽約)。

如果把開關、定時任務等resource也作為一種廣義的測試數據由數據銀行來管理,能實現測試用例儘可能並行執行。例如,有N個用例都需要修改一個開關值,這N個用例如果並行執行的話就會相互影響,他們相互之間應該串行執行。但N個用例中的任何一個,都可以和這N個用例之外的用例並行執行。數據銀行掌握了每個用例對各種資源的使用模式的詳細情況,再加上每個用例的平均運行時間等數據,就可以最優化、最準確的對一批測試用例進行編排,做到可以並行的都儘可能並行、不能並行的確保不併行,而且還可以在一個批次的執行過程中不斷的調整餘下還未執行的用例的編排。

這樣一個數據銀行是普遍適用的,不同業務之間的差異無非是具體的業務對象和resource不一樣。這些差異可以通過插件形式實現。如果有這麼一個通用的數據銀行[4],可以很方便的adopt,大量的中小軟件團隊的測試效率都可以得到明顯的提高。這樣的一個更加完備的數據銀行的想法,我到目前為止還只是想法,一直沒有機會實踐。

十 異常測試

一個分佈式系統,它的內部、內部各部分之間以及它和外部的交互都會出現各種異常:訪問超時、網絡連接和耗時的抖動、連接斷開、DNS無法解析、磁盤/CPU/內存/連接池等資源耗盡等等。如何確保系統的行為(包括業務邏輯、以及系統自保護措施如降級熔斷等)在所有的情況下都是符合預期的?今天我們的線上演練(本質上也是一種異常測試))已經做了很多了。如何把更多的問題提前到線下來發現?對於一個複雜的分佈式系統來說,要遍歷所有可能出現異常的地方和所有可能出現的異常,異常用例的數量是非常大的。此外,某些異常情況下,系統對外表現出來的行為應該沒有變化;而另一些異常情況下,系統行為是會有變化的。對於後一類,如何給出每一個異常用例的預期結果(即test oracle),也是比較有難度的。

十一 併發測試(Concurrency Test)

併發(concurrency)可能出現在各個level:數據庫層面,對同一張表、同一條記錄的併發讀寫;單系統層面,同一個進程內的多個線程之間的併發,單服務器上的多個進程之間的併發,以及單個服務的多個實例之間的併發;業務層面,對同一個業務對象(會員、單據、賬戶等)的併發操作,等等。傳統的併發測試是基於性能測試來做的,有點靠撞大運,而且經常是即便跑出問題來了也會被忽視或者無法repro。併發測試領域,我接觸過的一些成果包括Microsoft的CHESS以及阿里的譚錦發同學在探索的分佈式模型檢查&SST搜索算法。

十二 回滾的測試

安全生產三板斧宣傳了多年,在阿里經濟體內大家都能做到“可回滾”了。但我所觀察到的是:很多時候我們有回滾的能力,但是對回滾後系統的正確性,事前保障的手段還不夠。我們更多的是靠灰度和監控等事後手段來確保回滾不會回滾出問題來。事實上,過去兩年,我自己已經親身經歷過好幾次回滾導致的線上故障。回滾測試的難度在於:需要覆蓋的可能性非常多,一個發佈可能在任何一個點上回滾。回滾可能還會引發兼容性問題:新代碼生成的數據,在新代碼被回滾後,老代碼是否還能正確的處理這些數據。

十三 兼容性測試

代碼和數據的兼容性問題有很多形式。例如,如何確保新代碼能夠正確的處理所有的老數據?有時候,老數據是幾個月前的老代碼產生的,例如,一個正向支付單據可能會到幾個月以後才發生退款退票。有時候,老數據可能就是幾分鐘前產生的:用戶的一個操作,背後的流程執行到中間的時候代碼被升級了。驗證這些場景下的兼容性的難度在於:需要驗證的可能性太多了。今天的退款請求對應的正向單據,可能是過去很多個版本的代碼產生的。一個業務流程執行到中間具體什麼地方代碼被升級了,可能性也非常多。

異常測試、併發測試、回滾測試、兼容性測試,這些問題的一個共同點是:我們知道這些問題是可能存在的,但要測的話,需要測的可能性又太多。

十四 Mock

測試的有效性也依賴於mock的正確性。既然是mock,它和被mock的服務(包括內部的、二方的和三方的)的行為就多多少少會有差異。這種差異就有可能導致bug被漏過。前人也為此想出了“流量比對”等辦法。我曾經有另一個想法:“一鴨三吃”。也就是說,通過bundle和compiler instruction等方法,讓同一套源代碼支持三種不同的編譯構建模式:

  • 正常模式:這就是和今天的編譯構建是一樣的,產出的構建物是拿去生產環境跑的。
  • Mock模式:這個模式編譯出來的就是該服務的一個mock,但由於是同一套代碼編譯出來的,最大可能的保留了原來的業務邏輯,做到最大限度的仿真。而且由於是同一套代碼編譯出來的,後期也不會有“脫鉤”的擔心,應用代碼裡的業務邏輯變化都能及時反映在mock裡,大大減少mock的人肉維護工作量。
  • 壓測模式:這個模式編譯出來的也是一個mock,但這個mock是用來給(上游)做性能測試用的。過去在線下的性能壓測中經常遇到的情況是:我們想要壓的系統還沒到瓶頸,這個系統的下游系統(往往是一個測試環境)反而先到瓶頸了。壓測模式編譯出來的這個mock犧牲了一部分的業務邏輯仿真,但能確保這個mock本身性能非常好,不會成為性能瓶頸(但對lantency仍然是仿真的)。

這個“一鴨三吃”的想法so far還停留在想法層面,我還一直沒有機會實踐一下。

十五 靜態代碼分析

有一些類型的問題,要用通常意義上的軟件測試來發現,難度和成本很高,但反而是通過靜態代碼分析來發現反而比較容易。例如,ThreadLocal變量忘記清除,會導致內存溢出、會導致關鍵信息在不同的不同的上游請求之間串錯。另一個例子是NullPointerException。一種做法是通過fuzz testing、異常測試等手段來暴露代碼裡的NPE缺陷,以及可以在執行測試迴歸的時候觀察log裡面的NPE。但我們也可以通過靜態代碼分析,更早的就發現代碼裡面可能存在的NPE。有一些併發問題也可以通過靜態代碼分析來早期準確發現。總之,我們希望儘可能多的通過靜態代碼分析來防住問題。

十六 形式化驗證(Formal Verificaition)

除了在協議、芯片、關鍵算法等上面的運用以外,形式化方法在更偏業務的層面是否有運用的價值和可能?

十七 防錯設計(Mistake Proof)

嚴格來說,防錯設計並不是software testing範疇內的。但做測試做久了就發現,有很多bug、很多故障,如果設計的更好一點,就壓根不會發生(因此也就談不上需要測試了)。去年我總結了一下支付系統的防錯設計,後面希望能看到在各類軟件系統形態下的防錯設計原則都能總結出來,另外,最好還能有一些技術化的手段來幫助更好的落地這些防錯設計原則,這個難度可能比總結設計原則的難度更高。

十八 可測性(Testability)

雖然目前大部分開發和QA同學都知道“可測性”這麼件事情,但對可測性把握的還不夠體系化,很多同學覺得可測性就是開接口、加test hook。或者,還沒有很好的理解可測性這個東西落到自己這個領域(例如支付系統、公有云、ERP)意味著什麼。在需求和系統設計分析階段還不能做到很有效很有體系的從可測性角度提出要求,往往要求比較滯後。我希望可測性設計可以總結出一系列像程序設計的DRY、KISS、Composition Over Inheritance、Single Responsibility,Rule of Three等設計原則,總結出一系列的反模式,甚至出現像《設計模式》那樣的一本專門的著作。

以上就是我會加到Hard Problems in Test列表的問題,也是我已經或打算投入精力解決的問題。

注:

[1] 我工作中還有一些其他的測試難題,那些問題在這裡沒有列出來,因為那些問題和特定的業務場景或者技術棧的相關度比較高。還有一些測試領域的挑戰,難度也很高,例如,迴歸測試達到99%以上通過率、主幹開發以及做到通過代碼門禁的code change就是可以進入發佈的,這些也非常有難度,但難度主要是是偏工程的而不是軟件測試技術本身。
[2] 測試充分度的度量和提升是兩個問題。有一種觀點認為,測試充分的度量和提升其實是一件事情,用同樣的算法分析數據可以進行度量,也能用同樣的算法來基於數據進行測試充分度的提升。我不同意這個觀點。度量和提升未必是同一個算法。這樣的例子非常多了:測試有效性的度量和提升、運維穩定性的度量和提升,等等。即便度量和提升可以用同一種算法,我也希望可以儘量再找一些其他方法,儘量不要用同一種算法又做度量又做提升,因為這樣容易“閉環”和產生盲區和。
[3] 當然,這句話今天可能不再是那樣了,但那是十幾年前,那時候的在線廣告和大數據還沒到今天這個水平。
[4] 具體形式上,這個數據銀行無需是一個平臺。它不一定是一個服務,它也不一定需要有UI。它可以就是一個jar包,它可以就是在測試執行時launch的一個單獨的進程。

Leave a Reply

Your email address will not be published. Required fields are marked *