單元測試的藝術-讀後整理

Phineas
13 min readNov 12, 2019

--

前言

想當初,這本書剛發行沒多久,恰巧我人正在天瓏書局,恰巧剛好身上有一些錢,恰巧剛好又到了每一季我添購一些書的時候,恰巧那時候剛好工程師的魂又回復一些,對,在很多個恰巧後,這本書也就進入我的口袋名單之一,帶回家了。隨著時間一天又一天的過去這本書依舊在某一角落積灰塵,只差沒有當成泡麵的重物。某天,朋友分享參加公司補助的單元測試課程心得,聽完後,心裡多出一股憤世忌俗感(笑),找回這本書,研讀,也才有這一篇的出現。

一直以來都是知道測試是怎麼回事,能幫助什麼,大概要怎麼實作,以及其重要性。但,一來很多專案都是拋棄式的,實作到上架沒多少時間,加上完成後又會有新的案子準備進來。二來沒有對齊感到特別興趣,會想去研究 UI Test,因為這個功能除了測試的目的,還能設計成自動化來達到解決一些日常 routine 的事,反之 Unit Test 只是對 function 驗證其功能性,相比這兩個例子,真的就對 Unit Test 興致缺缺。不過,該來的還是要來,抱持著這種心情來看這本書,結果卻是大大的改變我對 Unit Test 的看法,扣除掉它的目的性,如何達到可測試性,重構程式碼,倒是真的讓我上了一課。

[Warning]

本文為許多單元測試文章名詞的總整理,故,許多名詞解釋脈絡或範例程式並非我所想,如想知道詳細,可以詳讀參考文獻連結。

本書學習目標

  • 單元測試
  • 說明單元測試與整合測試之間的差異優劣
  • 說明什麼是優秀的單元測試
  • 單元測試的三種驗證方式
  • 隔離相依的各種方式
  • 從單元測試的本質去挑選合適的隔離框架
  • 何謂反脆弱的單元測試三支柱(可信任,可讀性,可維護性)

單元測試具有 FICC 特性(Fast, Isolated, Configuration-Free, Consistent)

Fast

執行速度快

Isolated

即每個測試可以獨立執行,或是作為一組測試的一部分來執行,可以按照任何順序來執行。

Configuration-Free

不需要額外進行外部的設定

Consistent

產生穩定可靠的測試結果,包含通過和失敗

優秀的單元測試具備

  • 自動化
  • 容易被實現
  • 一段時間後還有存在意義(非臨時性)
  • 任何人都可以按個按鈕就能執行它
  • 執行速度快
  • 執行結果一致
  • 能完全掌控被測試的單元
  • 完全被隔離(執行時獨立於其他測試)
  • 執行失敗時,應該要很簡單的呈現問題的原因

先行名詞定義

Unit Test

一個單元測試就是一段程式碼,這段程式碼呼叫了另一段程式碼,然後驗證某些假設的正確性。如果這些假設是錯誤的,單元測試就會失敗。一個單元可以是一個方法或函數。

SUT(System Under Test)

被測試的東西。

整合測試

對一個工作單元進行測試,而這個測試對被測試的單元並沒有完全控制,而是使用該單元一個或多個真實依賴的相依物件,例如時間, 網路, 資料庫, 執行緒, 或亂數產生器等等。任何測試,假設他執行速度不夠快,結果不穩定,或是被測試單元要用到一個或多個真實相依物件,我認為他就是一種整合測試。

3A

  • Arrange — 初始化
  • Act — 行為,測試對象的執行過程
  • Assert — 驗證結果

測試常聽到的 3A 原則,或許用 given, when, than(should) 來解釋,也是不錯

  1. Given:對應到 Unit Testing 3A 中的 Arrange
  2. When:對應到 Unit Testing 3A 中的 Act
  3. Then:對應到 Unit Testing 3A 中的 Assert

Stub

Stub 通常使用在驗證目標回傳值,以及驗證目標物件狀態的改變。意即測試目標物件時,並不在乎目標物件與外部相依物件如何互動,關注在當外部相依物件回傳什麼樣的資料時,會導致目標物件內部的狀態或邏輯變化。所以這類的驗證方式,是透過 stub object 直接模擬外部相依物件回傳的資料,來驗證目標物件行為是否如同預期。Stub is used to provide indirect inputs to the SUT coming from its collaborators / dependencies. These inputs could be in form of objects, exceptions or primitive values. Unlike Fake, stubs are exercised by SUT. Going back to the Die example, we can use a Stub to return a fixed face value. This could simply our tests by taking out the randomness associated with rolling a Die.

我自己的解釋為當物件沒有參與互動,也不像是 spy 有記錄功能,測試時,需要靠 setter 更新資料,並且透過 getter 來取值驗證正確性,這種就是 Stub

mock

驗證目標物件與外部相依介面的互動方式。mock的驗證比起stub要複雜許多,變動性通常也會大一點,但往往在驗證一些void的行為會使用到,例如:在某個條件發生時,要記錄Log。這種情境,用stub就很難驗證,因為對目標物件來說,沒有回傳值,也沒有狀態變化,就只能透過mock object來驗證,目標物件是否正確的與Log介面進行互動。Like Indirect Inputs that flow back to SUT from its collaborators, there are also Indirect Outputs. Indirect outputs are tricky to test as they don’t return to SUT and are encapsulated by collaborator. Hence it becomes quite difficult to assert on them from a SUT standpoint. This is where behavior verification kicks in. Using behavior verification we can set expectations for SUT to exhibit the right behavior during its interactions with collaborators. Classic example of this is logging. When a SUT invokes logger it might quite difficult for us to assert on the actual log store (file, database, etc.). But what we can do is assert that logger is invoked by SUT. Below is an example that shows a typical mock in action

Spy

Spy 就是一個有記錄功能的 Stub,在測試函式的最後 可以問 spy 說哪個 method 被 call 了幾次 是給入哪些參數,反之 如果某些參數 你無法在測試前知道 你就只能用 spy 然後在測試的最後確認說那個函式有沒有被 call。Spy is a variation of behavior verification. Instead of setting up behavior expectations, Spy records calls made to the collaborator. SUT then can later assert the recordings of Spy. Below is variation of Logger shown for Mock. Focus on this test is to count the number of times Log is invoked on Logger. It’s doesn’t care about the inputs passed to Log, it just records the Log calls and asserts them. Complex Spy objects can also leverage callback features of moq framework.

Fake

當目標物件使用到靜態方法,或.net framework本身的物件,甚至於針對一般直接相依的物件,我們都可以透過 fake object 的方式,直接模擬相依物件的行為,Fake Obj 可以是各種 Unit Test 的物件,mock, Stub, Spy, 主要區分還是以各 Unit Test Obj 定義為主。Fake is used to simplify a dependency so that unit test can pass easily. There is very thin line between Fake and Stub which is best described here as – “a Test Stub acts as a control point to inject indirect inputs into the SUT the Fake Object does not. It merely provides a way for the interactions to occur in a self-consistent manner. These interactions (between the SUT and the Fake Object) will typically be many and the values passed in as arguments of earlier method calls will often be returned as results of later method calls“. A common place where you would use fake is database access. Below sample shows the same by creating a FakeProductRepository instead of using live database.

Dependency inject (DI)

此處舉例一個例子來至網頁,該功能主要目的在於透過抽出介面,讓耦合性降低,就不用產生物件,由外部注入,外部就可以決定,低耦合,提升可測試性。

小明是個愛乾淨的人,他工作時常加班導致房間雜亂,他不能忍受此狀況,所以小明去找一個清潔阿姨每天幫忙他打掃家裡,但,假設哪天阿姨哪天有事不能打掃,小明就必須要再去找人來幫忙打掃,由此可知小明耦合阿姨。如果,今天是小明把他要的條件給「打掃仲介公司」,仲介公司幫他尋找有沒有符合小明需求的打掃阿姨,假如今天 A 阿姨請假了,仲介公司會自動找另一個符合需求 B 阿姨幫忙打掃。
重構前 (https://dotblogs.com.tw/daniel/2018/01/17/140435)
重構後 (https://dotblogs.com.tw/daniel/2018/01/17/140435)

對小明來說,他需要的是一個人來幫忙他打掃,是哪個人倒是不重要,在使用物件上時,也如同工之妙,如果用抽象層來產生能達到的某件事的物件,這樣程式碼當遇到需求重大變更時,彈性會比較大,隨時能改。

常見解除依賴方法

在測試中,要解除對系統或其他物件的依賴,一般常見手法為兩種(仿間還有很多手法,如 dagger, etc, 這邊不討論),書中舉例兩種,分別是

A 型:具象類別(concrete class)抽象成介面(interfaces)委派(delegates)

B 型:重構程式碼,以便將委派或介面的偽實作注入至目標物件中(dagger 屬於此類別)

A 型:具象類別(concrete class)抽象成介面(interfaces)或委派(delegates)
B 型:重構程式碼,以便將委派或介面的偽實作注入至目標物件中

單元測試是否會搶了 QA 的飯碗

帶有全套的單元測試,可以讓 QA 在開始測試錢,確保所有的單元測試都能通過,這使得更能夠專心尋找真實世界情境中,更有邏輯性的 bug。單元測試能提供的僅為對抗 bug 的第一層防線,而 QA 則提供第二層:使用者驗收層,兩者不重複互相祓濯。

結論

本來想要把基本整套的單元測試文章寫完,但隨著撰寫過程,就算是快速帶過單元測試,內容還是非常多,故文章主題就隨著初探單元測試變成變成簡單名詞整理。讀過單元測試對我最大的收穫是如何重構程式碼,以前在撰寫程式碼時,總想著物件功能拆開,不要過多的 public function,能獨立拿到資料盡量不要用 setter 的方式塞入,當 feature 更動時,搬移程式碼會相對方便許多,不必到處複製冗長嘗試碼搬來搬去,但基於這種準則,發現不知不覺我自己的習慣已經把程式碼寫成不可測試,現在讀完一遍單元測試,受益良多,已經預排下一本書 ”重構” 到 TODO List。最後分享一句朋友的金句良言 “程式碼是照你寫的跑,而不是你想的跑”,人總是想得很完美,但現實是實作時,往往在思索過程中忽略邊界條件,而導致程式碼在一些狀況下,跑出非理想的行為,這時候單元測試就凸顯出其價值,無論何時何地,都能快速驗證功能的正確性。

--

--

Phineas

Garminer. 程式是照你寫的在跑,而不是依你想的在執行