91 的 Unit Test實戰營心得

Andy Lu
6 min readJun 11, 2020
Image by Nicole Turner from Pixabay

前言

K過很多程式經典書籍:Clean Code無瑕的程式碼Working Effectively with Legacy Code : 管理、修改、重構遺留程式碼的藝術,等等…,裡面一定會包含Unit Test(單元測試),在面對Legacy Code(遺留代碼)時,Unit Test是最重要的武器。但是這個這個這個主題很聽起來很容易,但是易學難精。

筆者自己本身也買了單元測試的藝術這本書,不過看了很久卻沒有辦法領悟它的精髓,在上週末有機會能夠參加由單元測試的藝術的譯者 — 91 — 擔任講師的Unit Test 一天課程。 上完一整天的課程之後,茅塞頓開,居然不覺得累。傑克,這真的是太神奇了。

(我是在今年一月報名的,等了超級久,不過等待是值得的,真的很值得!)

遺留代碼(Legacy Code):It often refers to code that’s hard to work with, hard to test, and usually even hard to read. — Roy Osherove (The art of unit testing: with examples in C#)

什麼是單元、單元測試又是什麼?

在單元測試的主題中,所謂的單元就是指一個情境,可以是一個方法,也可能大到是一個類別甚至多的類別。

所以單元測試,就是情境測試。在開發的時候,我們都是圍繞著情境、需求在進行開發,例如:按下登入按鈕,如果密碼錯誤就要跳出錯誤訊息,如果密碼正確,則可以正確登入。在還沒有寫下任何Unit Test的程式碼之前,測試方法的情境必須要先思考清楚,才不會落入不知道該測試什麼的情況。

單元測試的基本原則(F.I.R.S.T.)

  • Fast 快速:單元測試執行速度必須要很快,才不會慢到不想執行。
  • Independent, Isolation 獨立、隔絕:測試之間必須要互相獨立,一個測試不應該依賴另一個測試。
  • Repeatable可重複:每一次的測試結果在沒有修改程式之前,都應該是一樣的。
  • Self-Valid 自我驗證:可以反應出測試結果,並且在測試的報告可以看出結果。
  • Timely 及時:測試應該要在產品程式碼驗證完成之前寫完。

但是該怎麼進行單元測試呢?

這次的實戰營中主要分為兩個workshop,在這兩個workshop中,由簡單的類別到含有外部相依物件的類別,91 帶領我們一步一步思考如何針對不同複雜度的類別撰寫類別。

待測試的類別有兩種,一種是不含外部相依物件的類別,另一種則是含有外部相依物件的類別。

一開始,我們先從編寫一個不含外部相依物件的類別,待測試的方法中,裡面包含了一個if/else的判斷式。

思考

  1. sayHello()這個方法中,包含了if/else的判斷式,要如何才能夠測試兩種情境呢?
  2. 可以發現,if/else的判斷是根據LocalDate.now()取得的數值來判斷。
  3. 接續2,也就是說LocalDate today = LocalDate.now(); 是這個待測試方法的相依物件(Dependency)。

針對不含相依物件的類別,找到待測試方法的相依物件之後,我們就可以利用抽取及覆寫(Extract and Override)的方法來處理。

Unit Test 神兵利器 — 擷取與覆寫 (Extract and override)

在原類別

  1. 找出相依物件。
  2. 將相依物件抽取成一個private方法。
  3. 將該private改成protected的方法。

在測試類別

  1. 繼承原類別,命名為FakeGreeting。
  2. 覆寫FakeGreeting中的protected方法。
  3. 針對protected方法中的物件,建立其setter
  4. 在測試中,新建FakeGreeting的實例,然後根據情境修改不同的值。
  5. 測試。

在第二個Workshop中,這個類別含有多個外部相依物件,如下。

情境說明

  • 用LoginService的login方法進行login時,會先由ConfigDao取得ServerIP位置
  • 接者將serverIP、username以及password利用HttpClient呼叫loginServer進行登入

思考

  1. 在待測試LoginService的login方法中,有兩個外部相依物件:一個是ConfigDao, 另一個是HttpClient。
  2. 我們使用了Config的getServerIP方法,使用了HttpClient的loginServer方法。

針對含有外部相依的類別,我們可以使用相依注入(Dependency Injection)的方式來解決。

Unit Test 神兵利器 — 相依注入(Dependency Injection)

原類別

  1. 針對外部相依物件,建立其介面(Interface)
  • 例如:
  • Config ⇒ IConfig
  • HttpClient ⇒ IHttpClient

2. 將新增的介面,加上相依的方法

  • 例如:
  • IConfig: String getServerIP();
  • IHttpClient: bool loginServer(String ip, String username, String pwd)

3. 將原本使用相依物件地方,更改為使用其介面。

  • 例如
  • Config _config = new ConfigDao(); ⇒ IConfig config = new ConfigDao();
  • HttpClient _client = new HttpClient(); ⇒ IHttpClient client = new HttpClient();

4. 將步驟3修改的地方,抽取成類別變數。

5. 新增一個新建構子,分別注入兩個相依物件的介面。

6. 若原本沒有寫建構子,建立一個建構子,並在這裡初始化兩個相依物件。

最後,LoginService會變成

測試類別

  1. 利用Mock framework(Mockito)mock IConfig, IHttpClient
  2. 將mock的IConfig及IHttpClient代入LoginService
  3. 利用mock來模擬外部相依的行為
  • 例如:
  • when(_config.getServerIP()).thenReturn(serverIP);
  • 當mock的config呼叫getServerIP()方法要求取得serverIP,那麼回傳serverIP。

4. 如此呼叫LoginService時,裡面包含的外部輸入就會按照我們要求的輸出值,因此我們就可以避免使用外部相依來測試。

後記:

一天的實戰營下來,讓我把單元測試的藝術一書的一~四章看不懂的地方,用活潑生動的方式來讓我們了解。書中有些地方寫得比較詳細,常常就會卡在這個地方。經由親自寫單元測試,我們就可以更深刻的了解。

在聽老師的談話之間,可以發現老師真的是個熱血的人,希望我們能夠變強,而且是為自己變強。

那麼就讓我們一起寫Unit Test吧。

燃燒吧,我的小宇宙!!!

--

--

Andy Lu
Andy Lu

Written by Andy Lu

Android/Flutter developer, Kotlin Expert, like to learn and share.

No responses yet