前言
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的判斷式。
思考
- sayHello()這個方法中,包含了if/else的判斷式,要如何才能夠測試兩種情境呢?
- 可以發現,if/else的判斷是根據LocalDate.now()取得的數值來判斷。
- 接續2,也就是說LocalDate today = LocalDate.now(); 是這個待測試方法的相依物件(Dependency)。
針對不含相依物件的類別,找到待測試方法的相依物件之後,我們就可以利用抽取及覆寫(Extract and Override)的方法來處理。
Unit Test 神兵利器 — 擷取與覆寫 (Extract and override)
在原類別
- 找出相依物件。
- 將相依物件抽取成一個private方法。
- 將該private改成protected的方法。
在測試類別
- 繼承原類別,命名為FakeGreeting。
- 覆寫FakeGreeting中的protected方法。
- 針對protected方法中的物件,建立其setter
- 在測試中,新建FakeGreeting的實例,然後根據情境修改不同的值。
- 測試。
在第二個Workshop中,這個類別含有多個外部相依物件,如下。
情境說明
- 用LoginService的login方法進行login時,會先由ConfigDao取得ServerIP位置
- 接者將serverIP、username以及password利用HttpClient呼叫loginServer進行登入
思考
- 在待測試LoginService的login方法中,有兩個外部相依物件:一個是ConfigDao, 另一個是HttpClient。
- 我們使用了Config的getServerIP方法,使用了HttpClient的loginServer方法。
針對含有外部相依的類別,我們可以使用相依注入(Dependency Injection)的方式來解決。
Unit Test 神兵利器 — 相依注入(Dependency Injection)
原類別
- 針對外部相依物件,建立其介面(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會變成
測試類別
- 利用Mock framework(Mockito)mock IConfig, IHttpClient
- 將mock的IConfig及IHttpClient代入LoginService
- 利用mock來模擬外部相依的行為
- 例如:
- when(_config.getServerIP()).thenReturn(serverIP);
- 當mock的config呼叫getServerIP()方法要求取得serverIP,那麼回傳serverIP。
4. 如此呼叫LoginService時,裡面包含的外部輸入就會按照我們要求的輸出值,因此我們就可以避免使用外部相依來測試。
後記:
一天的實戰營下來,讓我把單元測試的藝術一書的一~四章看不懂的地方,用活潑生動的方式來讓我們了解。書中有些地方寫得比較詳細,常常就會卡在這個地方。經由親自寫單元測試,我們就可以更深刻的了解。
在聽老師的談話之間,可以發現老師真的是個熱血的人,希望我們能夠變強,而且是為自己變強。
那麼就讓我們一起寫Unit Test吧。
燃燒吧,我的小宇宙!!!