《Software Engineering at Google》ch 11 — Testing Overview

一個沒那麼肥的肥宅
今天的天空,有點藍
13 min readNov 9, 2021

前言:對於軟體工程的興趣隨著職涯的年齡與日俱增,恰好前陣子發現 Google 出了關於軟體工程的經驗談。《Software Engineering at Google》這一系列的文章是想分享我閱讀《Software Engineering at Google》這本書的筆記。透過汲取更多前人的經驗,來讓自己對於軟體工程方面的 scalability 能夠更有感觸。希望可以給自己的學習歷程留下一些什麼,也希望對想了解這方面知識的人有一些幫助。

關於測試這件事情,一直都是軟體開發的一環。在早期,軟體測試通常有著相當多手動的部分而且常常會出錯。然而,在 2000 年代之後,軟體產業的急速發展推動著測試的自動化,這樣的好處是可以讓工程師較早地發現 bugs,並且可以在重構、增加新功能等等會對程式碼進行修改的情況下,給予工程師足夠的信心。

越能夠快速迭代的公司,就越能夠快速適應科技發展、市場狀況以及客戶需求。倘若能夠確實實踐並執行足夠完整的測試,那麼工程師就可以較不畏懼改變,甚至能夠擁抱這樣的改變。此外,在開發階段就撰寫測試通常還會讓軟體更有模組化,因而擴充性也更增加。

雖然對於測試的討論如此眾多,但是要將這件事情做好卻是非常困難的事情。Google 做了許多的嘗試,但仍然需要面對許多的難題。本章節中想要分享的就是在這個過程中所學到的事情。

為什麼要寫測試

一個簡單的測試通常包含

  • 一個方法、函式或是API
  • 一個你傳給API特別的輸入值
  • 一個可以觀測的輸出或是行為
  • 一個受控制的環境,例如單一被隔離出來的程序

組合了數百個這樣的測試稱為測試套件 ( test suite )。測試套件能夠告訴你產品中那些部分符合你的初始設計,以及那些部分並不符合這樣的設計。隨著產品程式碼越來越大,測試也會越來越多,因此要特別注意那些不穩定或是跑的較慢的測試,不要讓他們成為開發的瓶頸。

在 Google,我們認為測試不能是事後才有的想法,我們學到了專注在品質以及測試是我們的職責,而沒有能夠做到這件事情會導致令人痛苦的後果,因此,我們已經將測試融入到公司的文化之中。

Google Web Server 的故事

Google 早期的時候,團隊們幾乎只依靠少數聰明人來將軟體寫得正確,而不在乎測試。許多系統都沒有較為完整的大型測試,GWS 尤其深陷這樣的問題之中。作為 Google Search 相當重要的一部分,GWS 卻在 2005 年遭遇到大小以及複雜度方面的巨幅增加,使得生產力大幅降低。此外,發行的版本經常有許多問題,而要推出發行的時間也越來越久,團隊對於改變程式碼都相當沒有信心,且經常在產品階段才會發現有些地方出了問題。為了解決這樣的狀況,GWS 的技術領導者訂定一個新的政策:當改動程式碼時都需要加上測試,而且這些測試都會不停地被執行。有趣的是,就這樣子不到一年的時間,緊急情況的次數就減少了一半。GWS 的經驗告訴了我們不該只依靠程式設計師去避免犯錯,因為即使每個人都非常聰明厲害,很少寫出 bugs,但是當人數一多,這些累積起來的 bugs 也會很可怕地影響著軟體開發。面對這樣的問題,最好的團隊會去找方法將累積的經驗轉化為團隊的知識庫來讓所有團員受益,而這正是自動測試所做的事情。

現今開發速度下的測試

現今的軟體系統越來越大且複雜,在 Google,一個典型應用程式或是服務通常是由數千到數百萬行的程式碼組成,他們可能使用了數百個函式庫或是框架,更糟的是,版本的更新越來越頻繁。只依賴人們手動去驗證系統的每個行為是無法追上 feature 擴增的速度的。因此,當討論到測試的時候,只剩下唯一一個可行的方式:自動化

撰寫、執行、回應

自動測試通常包含了撰寫測試、執行測試、以及執行完失敗的回應。範例 11就是一個非常簡單的自動測試。

Example 11–1. An example test (Software Engineering at Google)

撰寫測試僅僅只是第一步,通常還要做的是不斷地重複執行,關於這些將在第 23 章節 Continuous Integration 有更多的討論。藉由將測試用程式碼來表達而不是人為一步一步的手動操作,我們可以在程式碼有改動時就執行,甚至可以輕鬆地在一天之內執行數千次,而且最棒的是,與人為測試不同,機器永遠不會感到疲倦或是無聊。一個健康的自動測試文化會鼓勵人們分擔撰寫測試程式的工作,這樣的文化也會確保測試會定期執行,最後,為了維持軟體開發時的信心,也要多鼓勵工程師快速地解決失敗的測試。

撰寫測試程式碼的好處

對於那些沒有建立起足夠堅強的測試文化的組織來說,透過撰寫測試來增加產品開發速度聽起來有些矛盾。不過在 Google,我們認為投資在軟體測試有相當多的好處,包括減少除錯的時間、增加程式碼改動時的信心、更好的文件說明、簡單的審查、更徹底的設計以及快速且高品質的發行版本。

設計測試套件

在 Google,我們會將測試區分為兩種維度,分別是大小以及範圍。大小指的是跑完一個測試案例 ( test case ) 所需要的資源;範圍指的是想要驗證的程式碼路徑。

一個測試的大小不代表程式碼的行數,而在於它是如何被執行的、它被允許做什麼以及它會需要消耗多少的資源。一般來說,小型的測試會跑在一個程序裡面,中型的跑在一台機器上,而大型的則是完全沒有限制,如圖 11–2。

Figure 11–2. Test sizes

小型測試只允許被跑在單一程序 ( process )、甚至是單一執行緒 ( thread ) 之中。此外,小型測試也禁止使用 sleep、I/O 操作、或是其他 blocking 的呼叫。這些限制主要是為了確保小型測試能夠快速而且足夠穩定 ( deterministic )。雖然這樣的規範看似嚴格,但這卻是必須的,因為在 Google 這種規模的公司,即便只有一小部分的測試不夠穩定,也會造成許多 flaky tests 進而大幅降低生產力。

中型測試則可以接受多個程序、可以有 blocking 的呼叫,只不過依然不能夠進行 localhost 以外的 network 呼叫,這當然提供了更廣泛能夠測試的事情,但是與之相對的取捨就是速度降低以及不穩定性增加。大型測試由於完全沒有限制,彈性更高,可是因為巨大的不確定性以及速度最慢,因此 Google 的團隊們通常只會在建置或是發行階段才會進行大型測試。

不論何種測試,都應該要包含所有關於設定、執行、清除的必要資訊。此外,保持測試乾淨簡單能夠幫助審查者確認程式碼做的事情,因此我們常說「每個測試都應該要顯而易見」。測試也強烈反對含有 if-else 或是迴圈等的流程控制,因為越複雜的流程往往會讓測試失敗的時候難以除錯。

另一種分法是範圍。範圍指的是程式碼是如何被驗證的,小範圍的測試 ( 通常稱作 unit tests ) 主要是驗證一個類別或是方法等非常小的邏輯單元,中範圍的測試 ( 通常稱作 integration tests ) 被用來驗證一些元件的互動關係,如 server 與 database;大範圍的測試 ( 通常稱作 functional tests、end-to-end tests、system tests ) 則是用來驗證一個系統中好幾個不同的部分之間的關係。

如同我們鼓勵工程師去撰寫小型測試,我們同樣鼓勵工程師撰寫小範圍的測試,這樣的想法可以在圖 11–3 的測試金字塔中看到。反之,圖 11–4 則示範了兩種反模式。由於完成單元測試能夠提供我們高度的信心,且在開發早期就能做到,因此我們較為強調單元測試,以抓到許多程式碼中常見的邏輯問題。不過因為單元測試不能夠驗證所有元件之間的互動,因此一個好的測試套件是需要由不同大小與範圍的測試所混合而成。

Figure 11–3. Google’s version of Mike Cohn’s test pyramid;6 percentages are by test case count, and every team’s mix will be a little different (Software Engineering at Google)
Figure 11–4. Test suite antipatterns (Software Engineering at Google)

Beyoncé 規則

那麼到底那些行為應該要被測試的呢?簡單的回答是:你可以去測試任何你不想要被破壞的事情。換句話說,如果你希望一個系統能夠確切地展示某個特別的行為,唯一能夠確定如此的方法就是對它寫一個自動化的測試。我們甚至給了這樣的思維方式一個名字 — Beyoncé 規則

If you like it, then you shoulda put a test on it

代表的是如果你喜歡它,你就應該對它測試。

關於程式碼覆蓋率的小筆記

程式碼覆蓋率代表的是產品程式碼中有幾行有被納入測試之中的一種量測方式。這通常是對於了解軟體中測試品質的唯一標準,但卻也相當不幸的是,這只能保證程式碼的哪些部分有被執行到,卻不能保證哪些部份真的有被檢查。另一個更值得注意的是,當你訂定一個標準,比方說某個專案至少要有 80% 的覆蓋率,在工程師的眼中,那個數字很快就從門檻變成了天花板,從此之後覆蓋率永遠不會超過 80%,畢竟為什麼要做得比標準還要更多呢?

因此,真正重要的是測試套件的品質。多嘗試問問自己,產品是否確實有滿足客戶所需,或是是否能夠成功抓到不預期的改變?覆蓋率可以提供那些還沒有測試的程式碼一些參考的指標,但那不該是唯一的標準。

Google 規模的測試

大部分 Google 的程式碼都放在單一的儲存庫裡面 ( monorepo ),而這個儲存庫中擁有超過 20 億行的程式碼,每周都會有大約 2500 萬行程式碼的更動。這樣子開放的好處是當你發現有個產品有 bugs 或是服務有問題時,可以直接去修正它。另一個有趣的點是,在 Google 中幾乎沒有團隊使用分支 ( branch ),所有的程式碼更動都會直接提交到儲存庫的 head,而且會立即的被所有人看到。所有透過最新的程式碼更動的軟體建置,都會經過測試做到驗證。不論從任何角度,包括公司的規模、儲存庫、所管理的產品數量,Google 的工程環境都可以說是相當的複雜。

大型測試套件的陷阱

隨著程式碼數量日益增大,如果那些自動測試並沒有好好的被撰寫,導致當不相關的幾行程式碼改動時也會造成數十個測試失敗,那麼你就會感受到什麼叫做脆弱的測試了。過多脆弱的測試會導致團隊不願執行保持程式碼健康的重構,為了防止脆弱的測試,有許多需要注意的事情,其中必須值得一提的就是不要濫用模仿物件 ( mock ),這將在第 13 章節繼續說明。
除了脆弱的測試之外,大型的測試套件也會造成執行較為緩慢。只要執行越緩慢,就會越少被執行,那麼效益就會變得有限。無法保持測試套件的穩定性與速度,會成為生產力的絆腳石。因此,謹慎對待大型的測試套件才是真正的方法。將測試程式當作產品程式的重要性一樣看待,並且當遇到不如預期的測試時,花些精力改善那些不穩定與緩慢的狀況。
除了培養適當的文化,還應該要投資精力在開發 linters、文件、以及其他種種的協助來讓工程師很難寫出不好的測試,並且減少所需要使用的框架或是工具的數量以增加工作效率。如果沒有投資足夠的資源在能夠使得測試容易維護,那麼最後,工程師會覺得這些工作一點都不重要,而讓成果付之一炬。

Google 的測試歷史

如同前面所述,GWS 的歷史經驗告訴了我們自動測試的強大,而這樣的精神也影響著整間公司,自願者組成的測試小組 ( Testing Grouplet ) 推動了新人到職訓練、測試認證計畫與廁所裡的測試這三個關鍵的因素,大大地形塑了 Google 的工程文化。

新人到職訓練

測試小組了解到,按照公司成長的速度,新進來的工程師人數很快地就會超越本來就在公司的工程師們,因此只要能夠設法影響到這些人,對於公司文化就能有非常巨大的改變。而這最好的機會就是透過新人訓練。

大部分 Google 早期的新人訓練都是著眼於醫療的保障或是 Google Search 如何運作等等,但是自從 2005 年開始,新人訓練納入了長達一個小時關於自動測試的討論。在這樣的課程中介紹了測試的好處,包括增加生產力、更好的文件與對於重構的支援,以及如何寫出一個好的測試。這樣的課程對於那些新進的工程師們來說,會認為這些作法是公司內部習以為常的軟體開發方式。所以當這些新人各自分派到團隊之後,他們會開始撰寫測試,並且去質問那些不寫測試的人,進而養成團隊寫測試的習慣。如此一來,許多新的專案因此有了較好的出發點。

測試認證機制

在一開始的時候,許多大型且複雜的程式碼專案往往會拒絕導入測試,因為那些專案的程式碼品質可能不佳,以致於想要導入測試是一件相當困難的事情。測試小組編定了所謂的測試認證機制,等級從一到五,內容包含了連續建置、追蹤程式碼覆蓋率、將測試分為小中大的類型等等,一直到所有測試都是自動化、快速的測試會在程式碼提交之前等等,目標是給予團隊了解團隊目前所在的測試等級以及該如何更向前一步。

廁所中的測試

在測試小組所做的所有嘗試中,影響最深遠的大概莫過於廁所裡的測試 ( Testing on the Toilet, or TotT ) 了。測試小組在進行腦力激盪時討論著該如何更主動地提升整間公司對於測試的認知,有人提到了可以在廁所裡面放置宣傳廣告。這一開始只是當個笑話看待,但隨後測試小組就發現這完全是個非常聰明的點子,因為每個人每天至少會去廁所一次。因此,在 2006 年的四月,一張關於如何增進 Python 的測試出現在公司的廁所,雖然一開始引起了一些反對的聲浪,但隨著時間經過,越來越多人參與撰寫與張貼宣傳廣告的活動,TotT 也因而成為了所有測試小組所做的嘗試中影響最為深遠的一個。

今日的測試文化

從 2005 年以來,Google 的測試文化已經走過了漫漫長路,對於測試的認知也深深地影響著開發者日常的工作流程。每一個程式碼改動都會需要包含 feature 以及測試,並且需要經過程式碼審查。對於缺少測試的程式碼改動,是相當有可能會被擋下來的。

那麼為什麼我們不一開始就將撰寫測試作為義務呢?測試小組一開始曾經有這麼想過,但這立刻遭到反對,因為強迫別人該如何開發程式碼的指導都會嚴重地牴觸 Google 的文化。我們要相信成功的點子會逐漸散佈,因此專注於成功才是重要的事。

自動測試的限制

自動測試當然並不是萬能的。舉例來說,搜尋結果的測試需要仰賴人為的判斷、對於聲音和影片品質的細微差別通常也需要人為介入。因此這也是探索性測試的必要,當在執行探索性測試時,可以找到一些一開始未知的問題,當找到這些問題之後,可以再將其加入自動測試中以避免未來相同問題再次發生。

結論

開發者驅動的自動測試是 Google 最具變革的軟體工程實務之一,這讓 Google 有能力透過更大的團隊更快速地建立更大型系統,也能夠持續跟上科技變化的速度。儘管公司相較以往已經成長了數百倍,程式碼的品質以及測試都比過去的任何時候都還要更堅強。在接下來的章節中,我們將會進一步討論該如何寫出更好、更穩定以及更可靠的測試。

--

--