昨天我們講了針對 removeTag()
的單元測試
不過,如果我們考慮到針對 updateUsersTags()
的單元測試,我們會發現到一個問題。就是,這個函數他在設計時,就有 filter
這個傳入函數的參數。某種程度上來説,這個函數先天就必須和其他的元件互動。
這樣的情境下,我們還能做所謂的單元測試嗎?
這邊,我們就要介紹到一個新的觀念:測試假人(test double)了
剛剛我們提到,有的元件設計上就會需要和其他元件互動的問題。
遇到這個情境時,我們通常會想辦法設計一個假的元件,這個假元件是專門用在測試環境下,避免在測試階段需要和真實環境的元件進行互動。
在很多情境下會非常需要這類假元件,比方說我們有個和購物相關的邏輯,我們自然不會希望每次運作自動測試時,都會跑到正式的環境下嘗試去成立訂單,這樣的話會導致很多維護上的問題。
這類用來協助測試環境的假元件,我們統稱為 test double,對應英文的測試用假人
往下細分,test double 可以分為五類
下面我們花一點篇幅,來說明這五類的不同
dummy 物件,作為一個假物件,是 不做任何事情 的假物件。
通常來說這個物件只是用來放在參數列,讓函數可以繼續執行下去的。
舉例來說,如果我們當初的 updateUsersTags()
,不是寫成
fun updateUsersTags(users: List<User>, tags: List<Tag>, filter: List<Tag>.()->List<Tag> = {this}) {
transaction {
users.forEach {
it.tags = SizedCollection(tags.filter())
}
}}
而是寫成
fun updateUsersTags(users: List<User>, tags: List<Tag>, filter: List<Tag>.()->List<Tag>) {
transaction {
users.forEach {
it.tags = SizedCollection(tags.filter())
}
}}
那麼要讓他能繼續運作,就會需要 filter
裏面有值。
這時我們可能就會宣告一個
val filterDummy: List<Tag>.()->List<Tag> = {this}
updateUsersTags(users, tags, filterDummy)
讓程式可以繼續下去。
和 dummy 對應,反過來實作非常多功能的一種 test double,就是 fake 了。
fake 作為測試假體,是幾乎滿足正常環境的實作的。不過通常會省略一部分東西,導致適合測試環境,但是不適合正式環境。
比方說,很多後端網站在自動測試時,會選用 SQLite 來進行測試時的資料庫替代品。又或者我們之前的範例,所使用的 H2 in memory database,雖然裏面實作了資料庫的 CRUD 功能,讓我們可以順利測試,但是 in memory 的資料庫畢竟不太適合用在正式環境,所以正式環境我們切換成 MySQL。
這邊的 H2 in memory database,就是一種 fake。
stub 這個字,在英文裡對應是樹樁的意思。
顧名思義,這個東西就像是樹樁一樣,他沒有自己的邏輯判斷,只是在被敲打(呼叫)的時候,會傳出固定的聲音(回傳) 一樣。
舉例,我們如果希望測試在成立訂單時,如果訂單建立成功,我們的程式是否能正確地往下進行回傳,那我們可能就會建立一個 stub 物件,每次呼叫都固定回傳訂單建立成功的資訊。
spy 這個英文字大家可能很熟悉,就是間諜的意思。
這類 test double 不僅僅會像是 stub 一樣,每次被呼叫都回傳固定訊息,而且會紀錄下被呼叫的次數和被呼叫方式,像是一個間諜一樣。
舉前面的例子來說,如果我希望測試的項目不僅僅是訂單建立成功時程式是否能正確回傳,我還希望確認他嘗試建立訂單時所傳輸的資訊是正確的,這時我可能就需要建立 spy 物件,來協助我紀錄這些資訊了。
最後,我們提到 mock,這個字的意思是模仿,也是很多人在測試中常用的 test double。
和 stub 或 spy 不一樣,mock 物件是在一開始撰寫的時候,就需要設定預期被呼叫的次數與方式,如果測試內運行之後,呼叫次數或方式不符合的話,會拋出錯誤。
舉例來說,如果我們一開始就設定好,訂單要建立成功,需要呼叫元件一次,包含參數有哪些。那麼我們就可以用 mock 的方式來設定。這段測試如果沒有呼叫到 mock 物件時,這個物件會拋出錯誤,讓我們知道雖然我們程式其他的地方沒有問題,但是在呼叫元件的邏輯上是有問題的。
今天的觀念部分比較多,先讓各位讀者消化一下,我們明天見!