2017年10月10日 星期二

(二) Google軟體測試之道 -- SET



SWE是功能開發者,SET是測試開發者,TE是使用者開發者。
測試只是應用程式的另一項功能,而SET則是負責該測試功能的人。
為了能在早期就可以進行整合測試,SET會為每一個元件的相關部分,提供擬化與仿製模組。
過度投資在擬境測試自動化工作上,常會讓你被產品的某項特定設計所限制。
將程式碼檢查視為整個開過程的核心,比起程式碼的編寫,程式碼的檢查工作更為重要。
小型測試驗證一份單元程式碼的行為,中型測試驗證數個單元程式碼間的互動,大型測試則以整體的角度來驗證系統的運作。
小型測試可提升程式碼的品質,中大型的測試則可提升產品的品質。
SET面試官會注重面試者對解決方案所進行的思考,而比較不會拘泥在解決方案本身有多優雅。

工作流程
1.為寫在一個或多個原始檔中的服務,編寫一個類別或一組函式,並確保這些程式碼都可以編譯
2.為新服務指定一個程式庫建置標的
3.編寫匯入程式庫的單元測試、仿製其重要相依性,然後以具代表性的輸入執行最重要的程式路徑
4.為單元測試建立一個測試建置標的
5.建置並執行該測試標的,進行必要的調整直到通過所有的測試為止
6.執行所有必要的靜態分析工具,檢查是否遵守風格指引的規則,以及是否有產生一些常見的問題
7.將上述過程所產生的程式碼送交審查,經過適當調整後,重新執行所有的單元測試

各種範圍測試的優缺點

大型測試:
--針對終極的重要目標進行測試(應用程式運作得如何),負責外部子系統的行為
--因為無法獨立於外部系統之外,可能不具決定性
--範圍廣意味著當測試失敗時,不容易找到原因
--將測試情境所需的資料設定好,可能會需要消耗一些時間
--期待在高層次的操作能檢查到特定的少數案例,是比較不切實際的想法,這也是小型測試之所以存在的原因

中型測試:
--因為有較寬鬆的擬化需求與時間限制,可以讓開發團隊有個從大型測試過渡到小型測試的跳板
--執行起來相對較快,開發者可以常常執行
--在標準開發者環境中執行,開發者很容易就可以執行
--負責外部子系統的行為
--因為無法獨立於外部測試之外,可能不具決定性
--沒有小型測試那麼快

小型測試:
--會有比較簡潔的程式碼,因為方法必須相對地小與緊湊,讓測試得以容易地進行,因為在子系統間需要定義好介面,因此需要擬化模組
--因為執行得很快,臭蟲在早期就可以被處理掉,當程式碼有調整時,也可以有立即的回饋
--在所有的環境下都能可靠地執行
--有緊湊的範圍,讓臨界情況與錯誤條件(如空指標)這類的情況,可以容易地被檢測出來
--注重特定範圍內的狀況,容易將錯誤個別獨立出來處理
--不處理模組間的整合,這是其他類型的測試要處理的部分
--有時要將子系統擬化,會有一定的難度
--擬化或仿製環境可能會與真實情況連不起來

團隊的測試認證級別
LV1
--能設定測試涵蓋包
--能設定一個持續建置
--能將測試分類為小型/中型/大型測試
--能識別非決定性測試
--能建立一套煙霧測試套件

LV2
--提交前能通過一套煙霧套件的測試
--所有測試的涵蓋率增加到大於等於50%
--小型測試的涵蓋率增加到大於等於10%
--至少有一功能以整合測試來實作

LV3
--所有重要的調整都經過測試
--小型測試的涵蓋率增加到大於等於50%
--以整合型測試檢測新的重要功能

LV4
--提交新程式碼前自動執行煙霧測試
--煙霧測試的執行不能超過30分鐘
--排除非決定性測試
--整體測試的涵蓋率至少達到40%
--個別小型測試之測試涵蓋率至少須達到25%
--所有重要的功能都以整合型測試來檢測

LV5
--每一個重要的臭蟲修正都要加上測試
--積極地用可取得的分析工具
--整體測試涵蓋率至少須達到60%
--個別小型測試之測試涵蓋率至少須達到40%



測試一個函式 acount(void* s),功能是回傳在某個字串裡有多少個A:

基本的應徵者會透過一些發問與陳述來了解規則:
--輸入字串的編碼格式是ASCII、UTF-8、或其他的格式?
--函式的命名並不恰當,應該可以改成駝峰式的、更能表示函式功用的名稱,或者有其他的標準命名可供參考?
--回傳值的型別為何?
--使用void*滿不保險的,我們應該要指定合適的型別(如char*)如此我們就可以省掉編譯期的型別檢查
--怎樣才算是一個A?小寫的a也算嗎?
--標準函式庫裡找不到這個函式可用嗎?

好一點的應徵者還會:
考慮規摸: 也許回傳值的型別應該是一個64位元的整數,因為google常需要處理大量的資料
考慮再用性: 為什麼這個功能只能算有幾個A?可以計算用參數方式傳進來的字元個數之函式,應該會比分別編寫計算各個字元的函式要來得妥當
考慮安全性: 這些指標是否由安全的來源連結過來的

最好的應徵者會是:

考慮規模:
如果這個副程序都會被每一個google的查詢(query)所叫用,則它只會在使用安全的指標時被叫用,因為包裝程式(wrapper)已經做好驗證的工作了,也許避免進行一個空值檢查,一天下來將會省下數億次的CPU循環,而且降低一些使用者感受得到的延遲,至少也要了解一些教科書中關於參數驗證的意涵

考慮對不變動的部分進行最佳化:
--可以假設傳進來的資料已經排序好了嗎?如果可以,再找到第一個B之後就可以很快地跳離
--輸入資料有何種規律性,很常發生全部都是A的情況,還是大都為混合字元,或者只是A與空格所組成?如果是,我們在進行比較運算時就可以進行最佳化。在處理大量資料時,或者即使資料不多,程式碼在執行時,資料處理順序的改變,可能就會造成明顯得計算延遲

考慮安全性:
--在許多系統或者程式碼中對安全性比較敏感的部分,不要只考慮用非空值的指標來測試,1在某些系統中並不是一個有效的指標
--加入一個長度參數有助於程式碼不會取超過字串結尾的字元。合理地檢查長度參數的值。以空值結尾的字元串是駭客們最好的朋友
--函式執行時,如果緩衝區可能會被其他執行緒所修改,這就會產生執行緒安全性的問題
--我們是否該以try/catch的方式來進行檢查?或者如果叫用的程式碼並不預期會例外(exception),或許我們也應該要回傳錯誤碼給呼叫者。如果有這類的錯誤碼,那這些碼是否定義完整而且也已列在說明文件中了?



測試一個已經實作的函式:

int64 Acount(const char* s){
if (!s) return 0;
int64 count=0;
while(*s++){
if(*s == 'a') count++;
}
return count;
}

--能編寫解決問題的程式(也能改寫),熟習基本的語法,不會混淆不同語言的語法或關鍵字
--了解如何運用指標,也不會隨便配置不需要的東西
--會預先進行輸入值的驗證,以避免如參考到空值指標等原因而造成的惱人當機。或者在被問及時,能提出為何不做此類參數驗證的理由
--了解執行期或其他程式的BigO
--如果程式碼有些小問題被找到了,應該有能力更正
--能產出清楚、讓他人很容易看懂的程式碼
--用A或null為單一的測試値,讓程式碼走過一遍
--將一些假設條件與常數記下來,或者在程式碼寫上註解
--以許多不同的輸入測試程式,修正任何發現的臭蟲
--在被要求之前會主動測試他們的函式
--在被要求停止前,會持續測試,不斷地將解決方案優化

SET應該能以黑箱的方式,在別人實作函式的假設下進行測試。也要能以白箱的方式進行測試,了解哪一些測試案例沒辦法測出規格實作是否正確
--有方法且有系統。依據某些特性(如字串長度)來提供測試資料,而不會只用隨機的字串來測
--專注在產生有趣的測試資料,思考如何執行大型測試以及從哪去找真實的測試資料
--會啟動執行此函式的並行執行緒,以檢查是否有通串(cross talk)/死結(deadlock)/記憶體洩漏的問題
--建置長期執行的測試,如在一個while(true) 迴圈中執行測試,並確保其能在長期執行的情況下亦能運作正常
--對未來的測試案例以及測試資料的產生、驗證與執行中有趣的方法,保持興趣