筆者剛開始在公司加入自動化的單元測試時,發生過一個事件。
那時大部份的人對什麼是單元測試都還沒什麼概念,只知道這個新來的傢伙有在做這件事情。
不出意外的話馬上就要出意外了。有一天用戶回報一個系統上數據的問題,當然這個東西很重要,我們就趕快下去排查,沒過多久查到原因於是就趕緊修復就 Hotfix 上去了,當然也有補測試。
隔壁 Team 的同事耳聞這件事,下意識就問了一句:「咦,不是有自動化測試嗎?怎麼還會有錯?」那個時候我還很菜,一時之間也不知該怎麼回答,只覺得:「欸對呀!應該就是我學藝不精吧?就再加油囉!」
多年後的現在,我跟他都已離開那個單位很久,但,對方的問題,我想我可以回答了。
注意:這裡的自動化測試,泛指的是所有「過程中不用人眼看,跑完會告訴你哪裡有錯」的測試,而非單指特定測試分類。
在筆者從業這些年來,關於自動化測試,看過很多討論與案例。很多人很想做自動化測試但不知從何下手,很多人覺得自動化測試能解決所有問題,也有很多人開始做了自動化測試結果造成了更多問題。
以下,筆者先整理出一些自動化測試品質的度量的指標,與其常見的反模式,最後再來回答我前同事的問題。
不管你是單元測試、整合測試、End-to-End 測試,甚至是人為測試,任何「功能性測試」,因為你知道正確結果應為何,所以當你拿預測值來與實際值比較,其結果只會有四種。
圖片來源:ithelp
注意:這裡的 Positive,指的是「陽性」,也就是「發現有問題」的意思。一時間正負號可能有點難調整,各位可以慢下來先想一想。
最理想的狀態,就是「測出有錯真的有錯」(True Positive),以及「測出沒錯真的沒錯」(True Negative)。如果真能達到這樣,我們的測試(不管自動或手動),都能在任何場景,對產品的正確性給出最準確的指標。
True Positive 最單純,測試 Fail 了,我們就回頭看 Production Code,試圖找出 Production Code 產生測試預期外結果的原因。當我們找到,就代表這個測試真的有找出 Production Code 邏輯上的錯誤。這是我們最樂見,一般來說也最常見的事。
很遺憾地,True Negative 是所有功能性測試幾乎到達不了的境界。因為身為要達到「我說你對你就一定對」,這個測試就必須得「窮舉世上所有可能性」才行。有做過測試的人就知道,這是幾乎做不到的事,就算做得到也不實際。(試想,你不會打算窮舉所有 Integer 的值吧?)因此,對於 True Negative 這一塊,我們只能透過分析需求,以及「等價類」的妥善安排,來盡量減少出錯的可能性。
而功能測試會出問題,要嘛是「以為有錯其實沒錯」(False Positive),不然就是「以為沒錯其實有錯」(False Negative)。這兩種問題雖然都很難完全避免,但只要持續關注品質,總能隨著時間演進,慢慢發現,慢慢修正。
總之最理想的測試(雖然很難真的達成),就是所有情況都落在 True Positieve 與 True Negative 上,而因為 True Negative 可遇不可求,因此,我們可以說,在一般情況下,測試是一種對產品正確性的單向指標。也就是「我說你有錯,你就有錯,但我說你對時,不代表你一定對。」
然而,如果測試過程中,有「外部服務」的介入,就會使得我們對測試結果的落點無法判定。
例如,會不會其實程式是對的,但因為外部服務出了錯,而讓我們的測試報了錯?如果會,那我們就從 True Negative,掉到了 False Positive。
反過來,會不會其實程式是錯的,但因為外部服務出了錯,而陰錯陽差地,反而讓我們的測試通過了?如果會,那我們就從 True Positive,掉到了 False Negative。
更麻煩的是,因為外部服務非我們管轄範圍,因此就算我們發現他錯了,其實也做不了什麼事,只能通知對方改善。這對每天都要執行數十、數百次的自動化測試,影響尤其大。
因此,我們並不適合在測試過程中,依賴外部服務,就算在 End-to-End 測試中避免不了,那也影響不大了,因為健康專案中,End-to-End 的測試比重應該是輕的。
有人說,「我所有功能、所有類別,甚至所有方法與所有邏輯路徑都有測試保護,那我應該就安全了吧?」
不好說。
軟體是所有 Component 共同運行的。就算你所有 Component 都有單元測試,就算你測試也都非常完整,也不能保證整個系統運行起來會是對的。錯誤可能出現在任何地方, Component 之間的配合度如何、外部系統的健康度、營運環境的設定是否正確,這些都是影響系統表現正不正確的因素。
只測 Unit,但整合時出錯了,畫面取自華視新聞
因此,不能只因為測了單元測試,就覺得一定不會出錯,頂多只能說比較有信心。
相對於上一段的只測 Unit Test,也有人劍走偏鋒,只測 End to End。大部份場景下,這也不算是很好的習慣。
End to End 測試最大的問題是慢。我們都知道測試之間要完全獨立,因此每個 End to End 結束後,所有剛剛寫下的檔案或 DB Data 都要倒掉,或是重 build。同時,End to End 測試很常用到 GUI,例如網頁。為了測網頁,你的 web server、backend server、auth server… 等各種 server 都得啟一份。這個啟動時間也是很高的成本。
筆者曾在某個單位服務,該單位很注重測試,但測試的範圍都拉得很大,而且有 UI 的功能都會使用 Selenium 來輔助測試。當我離開時,那個測試任務跑一次要快一小時,而幾年後與一位晚我兩年離開的學弟聊天,他說後來該任務完整跑完一次已經要跑七八小時了。那幾乎是一天只夠跑一兩次的時間,這對現代的開發速度來說,可以想像是遠遠不夠的。
除了慢以外,還有一些場景是 End to End 測試很難創造的。例如 DB 連線失敗、網路突然斷掉,但重試到第三次突然又好了這種場景。不測,你又過意不去,要測,又很麻煩,你可能這個測試寫了 300 行,只是為了測一個 5 行的邏輯,也是不太划算。
照著程式寫測試,代表你已經知道程式的樣貌了。這時,你寫出來的測試,很容易被你的程式設計牽著鼻子走。也就是說,這樣的測試,測的是程式本身。
「測試測程式很合理,沒毛病呀!」
有的。測試是為了測出你的程式有沒有做到需求要的樣貌。比較好的做法,應該是照著需求,各寫一份測試與程式,然後讓兩者一起跑起來,如果對了就對了,如果錯了,要嘛程式錯,要嘛測試錯。這時,多半程式錯的機率大點,因為測試就比較好寫嘛!
至於要怎麼確保一定不會照著程式寫測試?
先寫測試囉!先寫測試的話,你沒有程式可以看,自然也就沒有上述的問題囉!
當你上面的事情都有遵守,該寫多的有寫多,該寫少的也有寫少,有一件事你一定要特別注意:
測試不能告訴你沒有 bug,測試只能告訴你有 bug,或是目前沒找到 bug。
筆者認為,軟體是一種科學,多過於是一種數學。數學的原理是可以「正反兩面證明」的,但科學只能反向證明,正向只能用大量的實驗來讓你相信。例如,蘋果拿在手上,放手會掉下來,這件事我無法用數學的方式證明給你看,沒辦法就是沒辦法,但我可以做實驗。
我先假設蘋果拿在手上,放手會掉下來,然後就在你面前拿起一個蘋果,放手,讓你看到它掉下來。放一次、放十次、放一千次,它都會掉下來。至此,你終於相信了「蘋果拿在手上,放手會掉下來」。這,就是一種科學。
軟體也是一樣,當你今天寫了一個銀行的存款功能,其正確性你是無法證明的。那怎麼辦?你就只能實驗了。你先假設這個功能是對的。然後你開始存錢。你存一次,正確,存十次,正確,存一千次、一萬次,結果都正確,那好,客戶就相信你,這個存款功能是正確的,他就能放心把錢存到你服務的銀行了。
事實上,誰有辦法每天、每小時,甚至每幾分鐘就存一次錢來做實驗?自動化測試是你最有效率,最經濟。不然,你以為我沒事寫什麼自動化測試?吃飽太閒喔?說到底還不就是為了實驗?
照著功能寫測試,照顧測試金字塔的形狀,不要以為測試過了就一定沒有 bug,因為軟體是一種科學。
謎之聲:「1 + 1 = 2 是數學,黑天鵝是被驗證為錯的假設,萬有引力則是至今沒被證明為錯,因此我們仍相信著的定律。」