第十一章:測試概述#

測試一直是軟體開發的一部分。早期的軟體測試大多仰賴人工、容易出錯,但自 2000 年代初期以來,業界的測試方法已大幅演進,以因應現代軟體系統的規模與複雜度。這場演進的核心,就是由開發者驅動的自動化測試(developer-driven, automated testing)。

自動化測試能在 bug 影響使用者之前攔截它們。開發週期中越晚發現的 bug,修復成本越高——在許多情況下呈指數成長。然而,「抓 bug」只是動機的一部分。同等重要的理由是:測試支撐了變更的能力。無論是新增功能、進行程式碼健康度重構,還是更大規模的重新設計,自動化測試都能快速發現錯誤,讓團隊有信心地修改軟體。能夠更快迭代的公司,能更迅速地適應不斷變化的技術、市場條件和客戶需求。如果你擁有健全的測試實踐,就不必畏懼變更——你可以將它視為軟體開發的本質特性。

撰寫測試的行為本身也能改善系統設計。作為程式碼的第一個使用者,測試可以揭示設計上的問題:系統是否與資料庫耦合過緊?API 是否支援所有必要的使用情境?系統是否處理了所有邊界情況?自動化測試迫使開發者在早期正視這些問題,通常能帶來更模組化、更有彈性的軟體。

在 Google,我們已經認定測試不能是事後的想法。將品質與測試融入工作方式,是我們工程文化的核心。我們曾痛苦地學到,未能在產品和服務中內建品質,必然導致糟糕的結果。

為什麼要寫測試?#

最簡單的測試由以下四個要素組成:

  1. 一個被測試的行為(通常是一個方法或 API)
  2. 一個特定的輸入值
  3. 一個可觀察的輸出或行為
  4. 一個受控的環境(例如一個獨立的行程)

將數百或數千個這樣的簡單測試匯集成測試套件(test suite),就能告訴你整個產品是否符合預期設計——更重要的是,何時不符合。

建立與維護健康的測試套件需要真正的投入。隨著程式碼庫成長,測試套件也會擴大,面臨不穩定和緩慢等挑戰。如果不處理這些問題,測試套件將失去效用。測試的價值來自工程師對它們的信任——如果測試成為生產力的黑洞,持續產生勞動和不確定性,工程師將失去信任並開始尋找繞過它的方法。一個糟糕的測試套件可能比沒有測試套件更糟糕。

除了幫助公司快速建造出色的產品之外,測試對於確保我們生活中重要產品和服務的安全性也越來越關鍵。軟體比以往更深入我們的生活,缺陷造成的影響遠不只是一點煩惱:它們可能造成巨額金錢損失、財產損失,甚至最糟糕的情況——生命損失。

Google Web Server 的故事#

在 Google 早期,工程師驅動的測試常被認為不重要。團隊仰賴聰明的人來寫出正確的軟體。其中受害最深的產品是 Google Web Server(GWS)——負責處理 Google Search 查詢的 Web 伺服器。

GWS 對 Google Search 的重要性,就如同空中交通管制之於機場。2005 年時,GWS 隨著規模與複雜度的膨脹,生產力嚴重下滑。發布越來越多 bug,推送時間越來越長。團隊成員對修改缺乏信心,經常在功能在生產環境中停止運作後才發現問題。一度有超過 80% 的生產環境推送包含影響使用者的 bug,必須回滾。

GWS 的技術主管決定推行工程師驅動的自動化測試政策:所有新程式碼變更都必須包含測試,且測試會持續執行。實施一年內,緊急推送次數減少了一半——儘管專案同期迎來了歷史新高的變更量。今天,GWS 擁有數萬個測試,幾乎每天發布,且鮮少出現使用者可見的故障。

GWS 的變革成為 Google 測試文化的分水嶺,其他部門的團隊看到測試的好處後,也開始採用類似的策略。

GWS 的經驗帶來了一個關鍵洞見:你無法僅靠程式設計師的能力來避免產品缺陷。即使每位工程師每月只寫一個 bug,一個百人團隊每個工作日仍會產生五個新 bug。更糟糕的是,在複雜系統中,修復一個 bug 往往會引發另一個,因為工程師會圍繞已知的 bug 編寫程式碼。

最好的團隊找到方法將成員的集體智慧轉化為整個團隊的利益——這正是自動化測試所做的事。一位工程師寫了一個測試後,它就被加入共同資源池。團隊中的每個人都可以執行這個測試並從中受益。相比之下,基於除錯的方法中,每次 bug 出現時,工程師都必須付出重新用除錯器深入調查的成本。這是 GWS 能夠扭轉命運的根本原因。

跟上現代開發速度#

現今 Google 的典型應用程式或服務由數千或數百萬行程式碼組成,使用數百個函式庫或框架,並透過不可靠的網路傳遞到越來越多的平台上。新版本頻繁推送,有時一天多次。

試想手動測試 Google Search 的所有功能——搜尋航班、電影時刻、相關圖片,以及當然還有網頁搜尋結果——再乘上每種語言、國家和裝置的組合,還要檢查無障礙性和安全性。手動評估產品品質根本無法擴展。答案只有一個:自動化

Figure 11.1: Screenshots of two complex Google search results

撰寫、執行、回應#

自動化測試由三個活動組成:

  1. 撰寫測試(Write):寫一小段程式碼(通常是單一函式或方法),呼叫被測系統的隔離部分,設定預期環境、傳入已知輸入並驗證結果。有些測試非常小,只驗證單一程式碼路徑;有些則大得多,可能涉及整個系統,如行動作業系統或瀏覽器
  2. 執行測試(Run):頻繁地、反覆地執行測試,只有在出錯時才需要人類關注。透過將測試表達為程式碼而非手動步驟,每次程式碼變更時都可以執行——輕鬆達到每天數千次。這就是持續整合(Continuous Integration, CI)
  3. 回應失敗(React):能在幾分鐘內修復失敗測試的團隊,能維持高度信心和快速的故障隔離。允許失敗的測試堆積,會迅速消耗測試提供的所有價值

將測試表示為程式碼的另一個好處是,它易於模組化以在各種環境中執行。在 Firefox 和 Chrome 中測試 Gmail 的行為不需要額外的工作量(只要有兩者的配置即可)。用日語或德語執行 UI 測試,也可以使用相同的測試程式碼。

健康的自動化測試文化鼓勵所有人分擔撰寫測試的工作,確保測試定期執行,並著重於快速修復失敗的測試以維持高度信心。

測試程式碼的好處#

對於來自缺乏測試文化的組織的開發者,「寫測試能提升生產力」可能聽起來矛盾。但在 Google,投資軟體測試帶來了以下關鍵好處:

  • 減少除錯:經過測試的程式碼在提交時缺陷更少,更關鍵的是在整個生命週期中缺陷都更少——大部分缺陷在程式碼提交前就已被捕捉。Google 的一段程式碼在其生命週期中預計會被修改數十次,會被其他團隊甚至自動化程式碼維護系統修改。一次寫好的測試會持續產生紅利,破壞測試的變更可以被測試基礎設施快速偵測並回滾,避免問題進入生產環境
  • 增加對變更的信心:擁有良好測試的團隊可以有信心地審查和接受變更。所有重要行為都被持續驗證,這鼓勵了重構。保留既有行為的重構變更(理想上)不需要修改現有測試
  • 改善文件:軟體文件是出了名的不可靠——從過時的需求到遺漏的邊界情況。清晰、聚焦、一次驗證一個行為的測試如同可執行的文件。想知道程式碼在特定情況下做什麼?看那個情況的測試就好。更好的是,當需求變更導致新程式碼破壞了現有測試,我們會得到一個明確的信號:「文件」已經過時了
  • 簡化審查:Google 的所有程式碼在提交前都需經過至少一位其他工程師的審查。程式碼審查包含完善的測試時,審查者可以驗證每個情況都有通過的測試,而不必費力地在腦中走過每個案例
  • 深思熟慮的設計:如果新程式碼難以測試,通常是因為被測程式碼有太多職責或難以管理的依賴。良好的設計應該是模組化的,避免緊耦合並專注於特定職責。在早期透過測試發現設計問題,往往意味著後期更少的返工
  • 快速、高品質的發布:Google 許多專案每天發布新版本——即使是擁有數百位工程師和數千個每日程式碼變更的大型專案。沒有自動化測試,這不可能實現

設計測試套件#

Google 今天的規模巨大,但並非一直如此,許多方法的基礎是很久以前奠定的——往往是在犯錯並事後清理之後。

早期的一個教訓是:工程師偏好撰寫大型的系統級測試,但這些測試更慢、更不可靠、更難除錯。被這些系統級測試的除錯折磨得受不了的工程師開始問自己:「為什麼我們不能一次只測試一個伺服器?」或者「為什麼我們需要一次測試整個伺服器?我們可以單獨測試更小的模組。」最終,減少痛苦的渴望促使團隊開發越來越小的測試,結果發現它們更快、更穩定、痛苦也更少。

這引發了公司內部大量關於「小」究竟意味著什麼的討論。小是指單元測試嗎?那整合測試是什麼大小?

Google 得出的結論是:每個測試案例有兩個不同的維度——大小(size)和範圍(scope):

  • 大小指執行測試所需的資源:記憶體、行程和時間
  • 範圍指正在驗證的特定程式碼路徑

請注意,執行一行程式碼與驗證它是否按預期工作是不同的。大小和範圍相互關聯但屬於不同的概念。

測試大小(Test Size)#

Google 將每個測試分類為一個大小,並鼓勵工程師針對特定功能盡可能撰寫最小的測試。測試的大小不是由程式碼行數決定,而是由它如何執行、允許做什麼、以及消耗多少資源來決定。簡而言之:

  • 小型測試(Small):在單一行程中執行
  • 中型測試(Medium):在單一機器上執行
  • 大型測試(Large):可在任何地方執行

Figure 11.2: Test sizes

Google 採用這種分類方式(而非傳統的「單元測試」或「整合測試」),是因為測試套件最重要的品質是速度確定性(determinism),不論測試的範圍為何。小型測試無論範圍如何,幾乎總是比涉及更多基礎設施或消耗更多資源的測試更快、更具確定性。

對小型測試施加限制使得速度和確定性更容易實現。隨著測試大小增加,許多限制被放寬。中型測試有更多靈活性,但也有更多非確定性的風險。大型測試則保留給最複雜和困難的測試場景。

小型測試(Small Tests)#

小型測試是三種大小中限制最多的。主要限制:

  • 必須在單一行程中執行(許多語言更進一步限制在單一執行緒)
  • 不允許 sleep、I/O 操作或任何阻塞式呼叫
  • 不能存取網路或磁碟

測試依賴這類操作的程式碼時,需使用測試替身(test doubles,詳見第 13 章)來以輕量的行程內依賴取代重量級的依賴。

這些限制的目的是確保小型測試沒有存取測試緩慢或非確定性主要來源的管道。在單一行程中執行且不做阻塞呼叫的測試,可以以 CPU 能處理的速度執行。這些限制乍看可能過於嚴格,但試想一個由幾百個小型測試案例組成、整天執行的適中套件。如果其中哪怕只有幾個出現非確定性失敗(即所謂的 flaky tests),追蹤原因就會嚴重拖累生產力。在 Google 的規模下,這樣的問題可能會讓整個測試基礎設施停擺。

Google 鼓勵工程師盡可能撰寫小型測試,不論測試的範圍為何,因為這能讓整個測試套件保持快速且可靠。

中型測試(Medium Tests)#

中型測試放寬了部分限制:

  • 可以跨越多個行程、使用執行緒
  • 可以對 localhost 進行阻塞式呼叫(包含網路呼叫)
  • 不能對 localhost 以外的系統進行網路呼叫

這開啟了許多可能性——例如執行資料庫實例來驗證程式碼在更真實的環境中整合正確性,或測試 Web UI 與伺服器程式碼的組合。網頁應用程式的測試通常涉及 WebDriver 等工具,它們會啟動真正的瀏覽器並透過測試行程遠端控制。

然而,靈活性增加也意味著測試變慢和非確定性的風險增加。跨行程或允許阻塞呼叫的測試依賴作業系統和第三方行程的快速與確定性,而這不是我們能保證的。中型測試透過阻止對遠端機器的網路存取來提供一些保護——這是大多數系統中緩慢和非確定性的最大來源。但在撰寫中型測試時,「安全鎖」是關閉的,工程師需要更加謹慎。

大型測試(Large Tests)#

大型測試移除了 localhost 的限制,允許測試和被測系統跨越多台機器。例如,測試可能針對遠端叢集中的系統執行。

如同前面所述,靈活性增加帶來風險增加。需要處理跨多台機器的系統以及連接它們的網路,相比在單一機器上執行,顯著增加了緩慢和非確定性的機率。Google 大多將大型測試保留給完整系統的端對端測試——這些測試更多是驗證配置而非程式碼片段——以及無法使用測試替身的遺留元件測試(詳見第 14 章)。團隊通常會將大型測試從小型或中型測試中隔離出來,僅在建置和發布流程中執行,以免影響開發者的日常工作流程。

案例研究:不穩定測試的代價#

如果你有數千個測試,每個帶有極少量的非確定性,每天執行下來,偶爾就會有一個失敗(稱為 flaky test)。即使每個測試只有 0.1% 的不當失敗率,每天執行 10,000 個測試,你就需要調查 10 個 flake。

在某些情況下,可以透過自動重新執行失敗的測試來限制 flaky tests 的影響,這實際上是用 CPU 週期換取工程時間。在低 flakiness 水準下,這種權衡是合理的,但要記住重新執行測試只是延後了處理 flakiness 根本原因的需要。

更糟糕的是信心的喪失。不需要調查太多 flake,團隊就會失去對測試套件的信任。一旦這種情況發生,工程師將不再回應測試失敗——測試套件提供的所有價值都將消失。Google 的經驗表明,當 flakiness 接近 1% 時,測試就開始失去價值。Google 的 flaky rate 維持在約 0.15% 左右,這已意味著每天數千個 flake,他們積極投入工程時間來修復它們。

不穩定測試(flaky tests)的主要來源是測試本身的非確定性行為——時鐘時間、執行緒排程、網路延遲等。良好的自動化測試基礎設施應幫助工程師識別並緩解這些非確定性行為。

所有測試大小的共同屬性#

  • 封閉性(Hermetic):測試應包含設定、執行和清理環境所需的所有資訊,對外部環境(如測試執行順序、共享資料庫)做最少的假設。這個約束在較大的測試中更具挑戰性,但仍應努力確保隔離
  • 清晰性:測試應僅包含驗證特定行為所需的資訊。清晰簡潔的測試有助於審查者驗證程式碼的正確性,也有助於在測試失敗時診斷問題。Google 常說「測試在檢查時應該是顯而易見的」。因為測試本身沒有測試,所以需要人工審查作為正確性的重要檢查
  • 避免控制流程:強烈不鼓勵在測試中使用條件判斷和迴圈等控制流程語句,因為更複雜的測試流程本身就有包含 bug 的風險,也更難判斷測試失敗的原因。記住,測試通常只在出錯時才會被重新檢視。程式碼被閱讀的次數遠多於被撰寫的次數——所以要寫出你自己願意閱讀的測試

實踐中的測試大小:擁有精確的測試大小定義使 Google 能夠建立工具來強制執行它們。這種強制執行使我們能夠擴展測試套件,同時仍然對速度、資源利用和穩定性做出某些保證。這些定義在 Google 不同語言中的執行程度各有不同。例如,所有 Java 測試都使用自訂的安全管理器執行,若標記為小型的測試嘗試做被禁止的事情(如建立網路連線),就會導致測試失敗。

測試範圍(Test Scope)#

測試範圍是指特定測試驗證了多少程式碼:

  • 窄範圍測試(即「單元測試」):驗證程式碼庫中小而聚焦的部分的邏輯,如單一類別或方法
  • 中範圍測試(即「整合測試」):驗證少數元件之間的互動,例如伺服器與其資料庫
  • 寬範圍測試(即「功能測試」/「端對端測試」/「系統測試」):驗證系統多個不同部分的互動,或無法在單一類別或方法中表達的湧現行為

值得注意的是,「窄範圍」指的是被驗證的程式碼,而非被執行的程式碼。一個類別可能有許多依賴或引用其他類別,這些依賴在測試目標類別時自然會被呼叫。雖然有些測試策略大量使用測試替身(fake 或 mock)來避免執行被測系統以外的程式碼,但 Google 偏好在可行時保留真實的依賴(第 13 章會更詳細討論這個議題)。

窄範圍測試傾向於小型,寬範圍測試傾向於中型或大型,但並非總是如此。例如,可以撰寫一個涵蓋伺服器端點所有正常解析、請求驗證和業務邏輯的寬範圍測試,但因為使用替身來取代所有行程外依賴(如資料庫或檔案系統)而仍然是小型測試。同樣地,也可能撰寫一個單一方法的窄範圍測試卻必須是中型的——例如,現代 Web 框架通常將 HTML 和 JavaScript 綁定在一起,測試像日期選擇器這樣的 UI 元件往往需要執行整個瀏覽器,即使只是驗證單一程式碼路徑。

正如 Google 鼓勵更小的測試大小,也鼓勵工程師撰寫更窄範圍的測試。作為一個非常粗略的指導方針,Google 的大致目標混合比例為:

  • 80% 窄範圍單元測試——驗證大部分業務邏輯
  • 15% 中範圍整合測試——驗證兩個或更多元件之間的互動
  • 5% 端對端測試——驗證整個系統

Figure 11.3: Google's version of Mike Cohn's test pyramid

單元測試構成了優秀的基礎,因為它們快速、穩定,並大幅縮小了識別類別或函式所有可能行為所需的範圍和認知負擔。此外,它們使故障診斷快速且無痛。

需要注意兩種反模式(antipattern):

  • 冰淇淋甜筒(Ice Cream Cone):大量端對端測試,很少整合或單元測試。這種套件往往緩慢、不可靠、難以維護。常出現在從原型倉促上線的專案中
  • 沙漏(Hourglass):大量端對端測試和單元測試,但很少整合測試。許多端對端測試失敗本可透過中範圍測試更快地被發現。沙漏模式常因緊耦合導致難以獨立實例化個別依賴

Figure 11.4: Test suite antipatterns

推薦的測試混合比例由兩個主要目標決定:工程生產力產品信心。偏重單元測試能在開發早期快速獲得高度信心,而較大的測試則作為產品發展時的健全性檢查(sanity check),不應被視為捕捉 bug 的主要手段。在考慮你自己的混合比例時,可能需要不同的平衡。如果你強調整合測試,可能會發現測試套件執行時間較長但能捕捉更多元件之間的問題。如果你強調單元測試,測試套件可以非常快速完成,且能捕捉許多常見的邏輯 bug,但無法驗證元件之間的互動。好的測試套件包含適合當地架構和組織現實的不同大小和範圍的測試組合。

Beyonce Rule#

Google 有一個測試哲學的名稱——Beyonce Rule

“If you liked it, then you shoulda put a test on it."(如果你喜歡它,就應該為它寫測試。)

簡而言之:如果你希望確信系統會展現某個特定行為,唯一確定的方式就是為它寫自動化測試。這包括效能、行為正確性、無障礙性、安全性,以及系統如何處理失敗。

Beyonce Rule 常被負責跨程式碼庫進行變更的基礎設施團隊援引:如果不相關的基礎設施變更通過了你所有的測試但仍然破壞了你團隊的產品,你就有責任修復它並添加額外的測試。

測試失敗情境:失敗是不可避免的,但等待真正的災難來發現系統如何回應不是好方法。應撰寫模擬常見失敗類型的自動化測試——包括單元測試中的例外/錯誤模擬,以及整合與端對端測試中的 RPC 錯誤或延遲注入,甚至使用 Chaos Engineering 技術進行更大規模的干擾。

關於程式碼覆蓋率的注意事項#

程式碼覆蓋率(Code Coverage)衡量的是哪些功能程式碼行被測試所執行。它常被視為理解測試品質的黃金標準指標,但這有些不幸。覆蓋率只測量一行程式碼是否被呼叫,而非呼叫後發生了什麼。

更隱蔽的問題是,覆蓋率和其他指標一樣,很快就會變成目的本身。團隊通常設定一個覆蓋率門檻——例如 80%。乍聽之下這完全合理:你當然希望至少有那麼多的覆蓋率。但在實踐中發生的是,工程師不是把 80% 當成地板,而是當成天花板。很快,變更開始以剛好不超過 80% 的覆蓋率提交。畢竟,為什麼要做比指標要求更多的工作呢?

更好的方法是思考被測試的行為:你是否有信心客戶期望的所有功能都能正常運作?你能否捕捉到依賴中的破壞性變更?你的測試是否穩定可靠?程式碼覆蓋率可以提供一些對未測試程式碼的洞察,但它不能取代對系統測試完整性的批判性思考。Google 建議僅從小型測試測量覆蓋率,以避免執行較大測試時產生的覆蓋率膨脹。

在 Google 規模下的測試#

要理解測試在 Google 如何運作,你需要了解 Google 的開發環境。其中最重要的事實是:Google 大部分程式碼保存在一個單一的單體倉庫(monorepo)中——幾乎每個產品和服務的每一行程式碼都存放在同一個地方,擁有超過二十億行程式碼。程式碼庫每週經歷接近兩千五百萬行變更——大約一半由數萬名工程師完成,另一半由自動化系統以配置更新或大規模變更(第 22 章)的形式完成。其中許多變更是由直屬專案之外的人發起的。

Google 對程式碼重用不設太多限制,開放的程式碼庫鼓勵了一種共同所有權(co-ownership)的水準:任何人都可以直接修復你使用的產品或服務中的 bug(當然需經過審批),而不只是抱怨它。這也意味著很多人會在他人擁有的程式碼庫部分進行變更。

另一個特點是幾乎沒有團隊使用版本庫分支(repository branching)。所有變更都提交到倉庫的 head,所有人立即可見。此外,所有軟體建置都使用測試基礎設施已驗證的最後提交。當一個產品或服務被建置時,幾乎每個執行所需的依賴也都從原始碼建置,同樣來自倉庫的 head。Google 透過 CI 系統管理這種規模的測試,其中一個關鍵元件是 Test Automated Platform(TAP)(詳見第 23 章)。

無論從規模、單體倉庫或提供的產品數量來看,Google 的工程環境都極為複雜。每週經歷數百萬行變更、數十億個測試案例執行、數萬個二進位檔案建置,以及數百個產品更新。

大型測試套件的陷阱#

隨著程式碼庫成長,你不可避免地需要修改現有程式碼。寫得不好的自動化測試實際上會使這些變更更加困難:

  • 脆弱測試(Brittle tests):過度指定預期結果或依賴大量複雜的樣板程式碼,即使不相關的變更也會失敗。如果你曾經只對一個功能做了五行修改,卻發現幾十個不相關的測試失敗,你就體會過脆弱測試的摩擦。隨著時間推移,這種摩擦會讓團隊不願進行保持程式碼庫健康所需的重構
  • Mock 物件的濫用:Google 的程式碼庫因過度使用 mocking 框架而深受其害,甚至有些工程師喊出了「no more mocks!」。理解 mock 物件的局限性有助於避免誤用
  • 緩慢的測試:測試可能因啟動大量系統、啟動模擬器、處理大型資料集或等待分散系統同步而變慢。測試通常一開始夠快,但隨著系統成長而變慢——也許你有一個整合測試依賴一個需要五秒回應的服務,但多年後你依賴了十幾個服務,同樣的測試現在需要五分鐘。sleep()setTimeout() 的不當使用也是常見原因:在這裡等半秒、那裡等半秒看起來不危險,但如果「等待並檢查」被嵌入一個廣泛使用的工具函式中,很快你的測試套件每次執行就多了幾分鐘的閒置時間。更好的做法是以接近微秒的頻率主動輪詢狀態變化,並搭配逾時值

未能保持測試套件的確定性和速度,將確保它成為生產力的路障。Google 的工程師遇到這類測試時,有些人甚至在提交變更時完全跳過測試。與大型測試套件共處的秘訣是像對待生產程式碼一樣對待測試:獎勵擁有穩固測試的工程師,設定適當的效能目標,重構緩慢或邊緣的測試,並投資測試基礎設施(linter、文件、工具)使寫出糟糕測試變得更困難。

Google 的測試歷史#

直到 2005 年,Google 的測試更像是一種好奇心而非紀律化的實踐。大部分測試是手動完成的——如果有做的話。然而 2005 至 2006 年間,一場測試革命發生了,改變了 Google 的軟體工程方式,其影響至今仍在公司內部迴盪。

GWS 專案的經驗(本章開頭所述)充當了催化劑,清楚地展示了自動化測試的強大力量。2005 年 GWS 改善之後,這些實踐開始在整個公司傳播。當時的工具很原始,但那些後來被稱為 Testing Grouplet 的志願者並沒有因此慢下腳步。三個關鍵倡議將自動化測試帶入了公司的意識中:

新人訓練課程(Orientation Classes)#

雖然 Google 早期的大部分工程人員都不重視測試,但自動化測試的先驅者知道,以公司的成長速度,新工程師很快就會超過現有團隊成員。如果他們能觸及公司所有的新人,這將是引入文化變革的極為有效的途徑。幸運的是,所有新工程師都會通過一個共同的瓶頸:新人訓練。

自 2005 年起,Google 的新人訓練開始包含一小時的自動化測試價值討論課程,涵蓋測試的各種好處(如提升生產力、更好的文件、支援重構)以及如何撰寫好的測試。關鍵在於,所有這些概念都被呈現為公司的標準實踐。對許多當時的 Noogler(新 Googler)來說,這是他們第一次接觸這些材料。最重要的是,新人完全不知道自己正被當作特洛伊木馬,把這個理念悄悄帶進毫無防備的團隊中。

隨著 Noogler 在新人訓練後加入團隊,他們開始寫測試並質疑那些不寫測試的同事。僅僅一兩年內,受過測試教育的工程師人數就超過了前測試文化的工程師,許多新專案因此從一開始就走上了正確的道路。時至今日,新人訓練課程仍在持續進行,將 Noogler 在 Google 外部已知的測試知識與在 Google 龐大而複雜的程式碼庫中實踐測試的挑戰連結起來。

Test Certified#

最初,程式碼庫中較大且較複雜的部分似乎對良好的測試實踐有抵抗力。有些專案的程式碼品質差到幾乎無法測試。為了給專案一條清晰的前進路徑,Testing Grouplet 設計了名為 Test Certified 的認證計畫,旨在讓團隊了解其測試流程的成熟度,更重要的是提供改善的具體指南。該計畫分為五個等級,每個等級需要一些具體行動來改善團隊的測試衛生。等級的設計使得每一步提升都可在一個季度內完成,方便配合 Google 的內部規劃節奏:

  • Level 1:設定持續建置、開始追蹤程式碼覆蓋率、將所有測試分類為小/中/大、識別 flaky tests、建立一組可快速執行的測試
  • 後續等級:加入更多挑戰,如「不得在測試失敗時發布」、「移除所有非確定性測試」
  • Level 5:所有測試自動化、快速測試在每次提交前執行、所有非確定性已移除、每個行為都有覆蓋

內部儀表板透過展示每個團隊的等級來施加社會壓力。到 2015 年被自動化方法取代時,Test Certified 已幫助超過 1,500 個專案改善了測試文化。

Testing on the Toilet(TotT)#

Testing Grouplet 最獨特的方法:在廁所隔間張貼測試相關的傳單。TotT 的目標很簡單:在整個公司提高對測試的認識。問題是,在一間員工分散在世界各地的公司中,最好的方式是什麼?

Testing Grouplet 考慮過定期電子郵件通訊,但鑑於 Google 每個人面對的大量郵件,它很可能被淹沒在噪音中。經過腦力激盪,有人半開玩笑地提出了在廁所隔間張貼傳單的想法。團隊很快認識到其中的天才之處:廁所是每個人每天都必須至少造訪一次的地方。

2006 年 4 月,第一篇關於改善 Python 測試的短文出現在 Google 各地的廁所隔間中。反應極度兩極化——有人認為是對個人空間的侵犯而強烈反對,郵件列表上充斥著抱怨——但 TotT 的創建者對此感到滿意:抱怨的人仍然在談論測試。

最終,風波平息了,TotT 迅速成為 Google 文化的標誌。至今,來自全公司的工程師已製作了數百集內容,涵蓋測試的幾乎每個面向(以及其他各種技術主題)。每集刻意限制在正好一頁,挑戰作者專注於最重要和最可行的建議。好的一集包含工程師可以立即帶回桌前嘗試的內容。諷刺的是,對於一份出現在最隱私場所的出版物,TotT 卻產生了巨大的外部影響——大多數外部訪客在造訪期間都會看到一集,TotT 的作者很早就開始公開發布經過輕微編輯的版本,幫助將 Google 的經驗分享給整個業界。TotT 是 Testing Grouplet 所有測試倡議中持續最久、影響最深遠的一個。

當今的測試文化#

Google 今天的測試文化已與 2005 年大不相同:

  • 每個程式碼變更都必須通過程式碼審查,且每個變更都應包含功能程式碼和測試。審查者有權在缺少測試時阻擋變更
  • Project Health(pH) 工具持續收集數十個專案健康指標(包括測試覆蓋率和測試延遲),以 1(最差)到 5(最佳)的標度衡量。pH-1 的專案被視為團隊需要解決的問題
  • 透過訓練、適度推動、指導和友善競爭的結合,Google 建立了明確的期望:測試是每個人的工作

為什麼不從一開始就強制要求寫測試? Testing Grouplet 曾考慮過請求高層領導發布測試命令,但很快放棄了這個想法。任何關於如何開發程式碼的強制命令都嚴重違反 Google 文化,可能會減緩進展——不管被強制的想法是什麼。他們相信成功的想法會自行傳播,因此專注於展示成功。如果工程師是自己決定寫測試的,就意味著他們已經完全接受了這個理念,並且即使沒有人強迫他們,也很可能會繼續做正確的事。

自動化測試的局限#

儘管自動化測試非常強大,但它並非適用於所有測試任務。某些類型的評估仍然需要人類的判斷:

  • 品質判斷:測試搜尋結果品質通常涉及人類判斷。Google 透過內部的 Search Quality Raters 進行有針對性的研究——他們執行真實查詢並記錄印象。同樣地,音訊和視訊品質的微妙差異也難以在自動化測試中捕捉,因此 Google 經常使用人類判斷來評估電話或視訊通話系統的效能
  • 創造性評估:搜尋複雜的安全漏洞是人類比自動化系統做得更好的事情。一旦人類發現並理解了一個漏洞,就可以將其加入自動化安全測試系統(如 Google 的 Cloud Security Scanner)中持續且大規模地執行
  • 探索性測試(Exploratory Testing):一種根本上具有創造性的工作,測試者將被測應用程式視為一個待破解的謎題,透過執行意外的步驟或插入意外的資料來發現問題。在開始時,待發現的具體問題是未知的,它們是透過探測常被忽略的程式碼路徑或應用程式的異常回應而逐漸被揭露的

與安全漏洞的發現類似,一旦探索性測試發現了一個問題,就應該添加自動化測試以防止未來的迴歸。利用自動化測試覆蓋已知的行為,使人類測試者的寶貴精力能專注於他們能提供最大價值的地方——同時避免讓他們無聊到流淚。

結論#

開發者驅動的自動化測試是 Google 最具變革性的軟體工程實踐之一。它使 Google 能以更大的團隊、更快的速度建造更大的系統,並幫助跟上不斷加速的技術變革步伐。在過去 15 年中,Google 成功地將測試提升為工程文化的規範——儘管公司規模成長了近 100 倍,對品質和測試的承諾今天比以往任何時候都更強。

本章旨在幫助你了解 Google 如何思考測試。在接下來的章節中,將更深入探討幾個關鍵主題:

  • 單元測試(第 12 章):Google 最常見的測試類型
  • 測試替身(第 13 章):如何有效使用 faking、stubbing 和互動測試等技術
  • 大型測試(第 14 章):測試更大和更複雜系統的挑戰

TL;DRs#

  • 自動化測試是支撐軟體變更能力的基礎
  • 測試要能夠擴展,就必須自動化
  • 平衡的測試套件對維護健康的測試覆蓋率至關重要
  • 「If you liked it, you should have put a test on it.」(Beyonce Rule)
  • 改變組織的測試文化需要時間,但其效果是深遠的