淺談依賴注入
昨天同事遇到了單元測試的問題,剛好與依賴注入(dependency injection)有關,因此剛好可以騙一篇文章來介紹與討論這個名詞。
在開始說明前,先來思考下面這個 Kata。這是教我單元測試的師父出的題目,但後續有跟其他社群朋友討論後,才知道原來這是 Joey Chen 前輩教材的一部分:
請實作可以判斷今天是否是聖誕節的程式,並對它寫個單元測試。
實作
這程式太簡單了,三秒鐘就能搞定!
function isTodayXmasDays(): bool |
簡短到不需要註解說明,用 PHP 原生 DateTime 類別加上 format 方法就搞定了。
但,這個 Kata,難的是測試。下面有兩種 PHPUnit assertion 寫法,請問應該要用哪一種?
public function testIsTodayXmasDay() |
使用 assertTrue,那這個測試只有在 12/25 那天才會 pass;使用 assertFalse 則相反,在 12/25 那天會壞掉,而且那天還沒有放假!
來回頭看單元測試應該要有的五個原則--F.I.R.S.T. 原則:
- Fast 快
- Independent 獨立,測試間互不依賴
- Repeatable 任何環境都能正常執行
- Self-Validating 直接回應驗證的結果
- Thorough / Timely 確保程式與測試是一起完成的
這個程式無法符合 Self-Validating 特性,因為它無法「直接」反應,必須要時間到的時候才能反應,因此就會造成上述問題。
這個測試程式不符合 Repeatable 原則,因為它需要特定環境(指時間)才能正常執行,因此就會造成上述問題。
延伸思考:即便不是使用單元測試,透過驗收測試也會有一樣的問題。
Why?
為什麼會這樣?因為實作裡面有一段程式的內容是無法控制的,另一個說法則是它有副作用(side effect)。這段內容指的就是 new DateTime()
--我們沒辦法控制「今天」是什麼時候。即便換成 date() 或其他函式,都有一樣的問題。
那該怎麼寫測試呢?這時候就輪到依賴注入出場了。依賴注入如其名,依賴在別的地方準備好後,再交給實際的程式使用。這段實作依賴了「今天」的內容,因此我們把今天的內容改成注入即可。
依賴注入的形式
在物件導向設計的實作裡,依賴注入的方法有三種。
參數注入
最簡單暴力的方法,直接加一個新的參數,在呼叫函式的時候,同時把依賴也給函式:
function isTodayXmasDays(DateTime $now): bool |
測試的時候,只要根據物件類型來 mock 即可。
// 預期是聖誕節 |
建構子注入
在建構(new)的時候,同時把依賴放在自己的肚子裡,等呼叫對應的方法時再拿出來用。上例程式是函式,所以必須要改寫成類別才能使用這個形式:
class Xmas |
測試的時候,在建構同時給日期的物件:
$mock = Mockery::mock(DateTime::class)->shouldReceive('format')->willReturn('1225'); |
Setter 注入
與建構子注入差不多,只是改由另一個方法來處理這個任務:
public function setNow(DateTime $now) |
測試程式則需要額外呼叫 setter 方法:
$mock = Mockery::mock(DateTime::class)->shouldReceive('format')->willReturn('1225'); |
結論
主要的方法就這三種,其他框架可能有更方便的方法,但原理都是利用其他機制(如反射)來自動處理或選擇上面的方法來達成最後的結果。
三種方法的選擇,我的經驗如下:
- 參數注入用在,只有該方法會需要用到此依賴的時候用的,但這個做法可能會有隱含問題,要確認的是方法與主要類別是否有內聚關係。
- 建構子注入用在,依賴與主要類別是強依賴關係。
- Setter 注入用在,依賴與主要類別是弱依賴關係,這個做法可能也會有隱含問題,要確認依賴跟主要類別的關係是否可以透過第三方(如 Context)來隔離。
最後,這個技巧可以有效應用在單元測試與設計模組上。如果想把程式寫好的話,建議上面三種方法要實際自己多嘗試,才會了解如何選擇與應用。