經過了差不多兩週,我認為監控和警報的部分總算是告了個段落,雖然之後還需要透過實際的收集監控資料來修正警報的規則跟 dashboard 就是了。接下來剛好我是讀到有關測試的部分,那我今天就來談談這個吧。
因為 SRE 關注的是可靠性,而測試便是一個非常直接的手段,它可以幫助我們去判斷在每次更新前後,服務的行為是否仍然如同我們的預期,想當然爾,為了避免人力需求與專案規模一同線性成長,這些流程都要盡量的自動化。
不過需要注意的是,測試也不是萬靈丹,通過測試並不意味著軟體沒有任何 bug,即使覆蓋率達到 100% 也是如此,因為我們通常沒有辦法確定所有輸入在所有情況下的行為。但至少,沒有通過測試可以幫助我們即時發現即將產生的不穩定性(如果它被部署下去的話)。
在 SRE book 裡面,把測試分成兩類:傳統測試與正式環境測試,其中傳統測試表示的是在服務部署下去之前所進行的那些測試,主要用於評估服務的正確性,而正式環境測試則是對於已經部署下去的服務能否正常運作。
不過看到這邊我倒是有點疑惑,因為壓力測試被列在了正式環境測試底下,然而就我的理解,應該是不會直接對生產環境這麼做?應該會在另外的專門用來測試的環境去做。
那麼下面就簡單列一下各種測試與目前在 NOJ 上的情形(雖然大部分是都沒有做啦)。
單元測試我想應該算是在所有測試類別之中最簡易最輕的一種類別了,主要就是希望可以針對「單元」去驗證它的行為,可能是類別或是函式。以這部分來說我想 NOJ 其實算是勉強有做,只是有些缺點尚需改進。
首先第一點是測試案例之間有相依性,有部分測試案例的正確性,是取決於前面測試的通過與否,像是這個,它的概念大概像是這樣。
def test_signup(self, client):
'''
Sign up user "test1" and "test2"
Assert it will success
'''
def test_used_username(self, client):
'''
Sign up user "test1"
Assert it will fail
'''
上面的兩個案例,其中 test_signup
會在資料庫裡面去註冊兩個使用者,分別是 test1 和 test2,然後在 test_used_username
裡面去嘗試註冊 test1 這個名稱,並且預期它會因為重複命名而失敗,然而這件事情的前提是,我們已經在資料庫裡面存在 test1 這位使用者。
這在這邊或許是影響不大,因為兩個測試案例離的非常的近,內容也不多,所以或許我們可以很快地看到 test_signup
與 test_used_username
之間有所關聯。但我想這件事在測試案例逐漸龐大的時候,會讓整份測試變得難以維護,因為我們可能會不小心在任意測試案例之間存在相依關係。若是沒有在文件裡面註明這些前提的話,可能會導致測試裡面也容易存有 bug。
順帶一提,我認為在設計上不要做太多對於環境的「假設」,可以寫出更易於測試的架構與更加穩定的測試。
另一個問題是,在現行的測試裡面,我們測試的行為都專注在 web API 上面,然而這件事代表它會引入額外的依賴,也就是需要有 flask 的 server 才能執行操作。可是比較好的做法應該是讓這些操作可以透過 python 的函式呼叫完成,可以避免在處理 HTTP 請求的時候意外造成一些 bug,也可以縮減 bug 的存在範圍。
然而這個問題也有關於軟體本身的架構,因為我們一開始在設計上並沒有考量到這些,所以有不少操作(e.g. 創建 submission)都只能透過 HTTP 請求完成,因此從撰寫測試的過程中,其實也可以檢視架構的好壞。
整合測試所希望測試的對象,是由已經通過測試的各個單元所組成的一個整體,因為單元測試只考慮單元本身,有時候有些 bug 是需要組合多個單元才會發現的。根據前面的定義,這表示整合測試可能會涉及多個不同的類別,另外針對如何解決相依關係,比較常見的解法包含依賴注入或是 mock,可以更好的去控管我們測試的依賴。
然而在做 mock 的時候也要小心,mock 過後的行為是否真的會與本來保持一致,以 NOJ 用來 mock MongoDB 的 mongomock 為例,這個 issue 導致我在寫入資料的時候可能會造成錯誤,但是 MongoDB 應該是不會有這問題的。
話說有關於單元測試與整合測試的分界,似乎一直是個困難的議題,我在搜尋資料的時候看到這篇文章寫得挺好,大家可以參考看看。
其他書上所列的測試我看了一下,NOJ 應該是通通沒有做。包含冒煙測試、效能測試、金絲雀測試等等。以目前的情況來說,我想先把單元測試(或許還有整合測試)寫好應該對於提升整個專案的穩定性來說是比較有效益的選項。
然後,我認為不管測試類型,對於每個已修復的問題或是新開發的功能,都補上對應的測試應是一個提升穩定性的有效且較低成本的方式,因為在那當下,是我們對於各種 context 最熟悉的時機,若是打算拖到之後再補的話,通常就是不會補的意思(除非在那個地方被人戳出 bug 才有機會)。