[30天快速上手TDD][Day 4]是否需針對非 public method 進行測試?

[30天快速上手TDD][Day 4]是否需針對非 public method 進行測試?

前情提要

在上一篇文章:[30天快速上手TDD][Day 3]動手寫 Unit Test 中有提到,VS2012 將原本測試非 public method 的功能移除了。

而今天也剛好有朋友問到,為什麼這麼好用的功能已經作出來了,還特地要移除呢?測試非 public 的方法,會有什麼問題嗎?

我連非 public 的方法都測了,品質應該更好吧?

這一篇文章,將從物件導向的觀點,以及維護的成本進行一些說明。也希望可以替心裡有這些疑惑的朋友們,帶來不一樣角度的思考。

本篇文章絕大部分與之前這篇文章相同,但為了讓整個系列更加完整,故筆者還是將此篇文章安插在 Day 4 ,並針對這個系列中,針對非 public method 進行測試,會有什麼影響,筆者額外補充了一些東西,使本篇文章更能承上啟下。還望讀者朋友們海涵。

 

前言

在 Visual Studio 2012 中,針對 Unit Test 的部份,有一個重要的變動:

原本針對「測試物件非 public 的部分」,開發人員可透過 Visual Studio 2010 自動產生的 accessor 來進行測試。但在 Visual Studio 2012 中,將此功能移除了。

Accessor 其背後的原理,是將物件透過很「髒」的 reflection 方式,把物件內所有的東西 public 出來。並且 Visual Studio 在異動物件後,進行與設計測試時,會幫你做同步產生 accessor 的動作。(實際的原理我沒有深入研究,也不太確定。但基本上的概念就是如此)

這個原本被認為很方便、實用的功能(包括我很久之前寫測試時,也是這麼認為),很抱歉,在 Visual Studio 2012 中已經被移除了。

接下來本篇文章將會說明,單元測試是否應該對測試物件非 public 的部份,進行單元測試。

 

單元測試的意義

一言以蔽之:「單元測試就是用來模擬外部如何使用測試目標物件,驗證其行為是否符合預期」。

因此,有個重點是:外部如何使用測試目標物件

讓我們回到 Object-Oriented 的封裝原則,封裝的用意在於:

  1. 隔離出物件的內部與外部。也就是定義「物件的邊界」,以及定義「外部可視部分」。
  2. 將外部使用端,不需要了解物件的內部資訊,封裝起來。也就是「封裝細節」。
  3. 將物件內部的變化,封裝起來。也就是「封裝變化」。

有了對單元測試與封裝的認知後,接下來說明,為什麼單元測試只需要針對測試目標物件 public 的行為,進行測試即可。

 

只測試 Public 行為?

根據單元測試的意義,以及封裝的用意,代表著「外部使用者原本就不需要了解,也根本不了解,測試目標物件非public的行為」。單元測試既然是模擬外部使用端的動作,那當然只針對測試目標物件 public 的行為進行模擬與驗證。

但一些朋友肯定有些疑惑,那非 public 的 method 該怎麼辦?不測嗎?那 code coverage 怎麼提昇?要怎麼知道這些非 public 的行為有沒如同預期般運作呢?

有這些疑問是正常的,因為我一開始也是有一模一樣的疑問,但開始接觸 TDD 之後,反而更加了解了 Unit Test 的本質。

所謂的非 public 的行為,其存在的原因,一定是因為某一些 public 的行為會用到這些 private 或 protected 的 method,如果物件中存在著跟 public method 無關的 private 或 protected method,那在設計上就是個問題,這些非 public 的 method 根本就沒有存在的意義。因為外部使用測試目標物件時,完全不會用到這些 method,就像宣告了變數卻不去使用它一樣,沒有意義。

而當 private 或 protected method 與 public method有關時,那針對 public method 的 Unit Test 便會涵蓋到這些 private 或 protected method,它們就是 public method 的一部分,對外部使用者來說,根本分辨不出來什麼是 private 或 protected,因為只關注在物件外部可視行為上。

所以,在實作單元測試上,倘若測試物件一個 public method 中,涵蓋了一個 private method,而 private method 中與外部物件或服務相依,那麼在測這個 public method 時,要連 private method 中相依的 interface ,都要撰寫 stub object 來模擬才行,這也是為什麼單元測試被稱為白箱測試的原因。但還是得強調一次,外部使用者是無法分清楚哪一部分是 public method 內容,哪一部分是非 public method。

總結上面的說法,非 public method 的測試涵蓋率,是依據 public method 呼叫時的 input 來決定。

有沒有可能,當 public method 該測的都測了,甚至 public method 主體內容涵蓋率都 100% 了,非 public 的部分涵蓋率卻很低?當然有可能,但這要釐清一下,沒有被涵蓋到的部份,是屬於什麼樣的程式碼。

如果在非 public method 中,沒被測試覆蓋的部份,是防呆、斷言之類的程式碼,那麼是屬於正常的情況。因為可能在呼叫非 public method 之前,就已經先防呆了,導致非 public method 中的防呆永遠不會發生。但,因為系統的健壯性考量,該斷言、防呆、驗證的部份,還是不能少。因為不會知道未來其他方法呼叫前,有沒做好防呆的部份。

那麼,在 private 或 protected method 中,非防呆、斷言的程式碼,卻又沒被涵蓋到部分呢?這是個警訊,代表著這些程式碼可能是 over design,或是根本沒有用處。因為這個物件所有對外的行為,所有的可能性,都模擬過一次了,卻都不會用到這些沒被涵蓋到的程式碼,這不就代表「這些程式碼目前用不到」嗎?YAGNI 原則就是在說這件事:「You ain't gonna need it !

只要 public 的行為如同預期,即使 private 或 protected 的 method 是 hard-code,是很沒彈性,是很愚蠢的寫法,對外部使用來說,根本就不在乎,因為無感。

這也是 TDD 所提倡的精神,如果所有使用行為都符合預期,就代表功能完成了。而且依據測試來撰寫的 production code,幾乎不會出現測試涵蓋不到的 code,因為 production code 是為了滿足測試而撰寫的。不需要存在用不到的 production code,因此,也可以避免 over design 的情況。

 

針對非 public 行為測試又如何?

上面那一段的說明,肯定還是無法說服所有人,「為什麼要把已經存在的功能移除?」

不用 accessor 的人大可不用,但已經在用,或真的得用的人,還是希望可以在 VS2012 中繼續使用。

回到封裝的用意上,「封裝變化」一直是物件導向設計中很重要的設計原則。那些針對 private 與 protected 進行單元測試的朋友,有沒有過「因為一些需求異動,導致單元測試程式就需要跟著重新調整、設計或修改,而且頻率與範圍導致測試的維護成本增加不少」的經驗。如果有,這就是為什麼不希望 developer 去針對非 public method 寫單元測試的原因。

著重在非 public method 的單元測試,說穿了只是寫給 developer 爽而已。因為要封裝變化,才會把這些內容變成 private 或 protected,以期望變化時對外部使用者來說,呈現無感,也就是降低耦合,也就是最小知識原則。

現在單元測試卻透過某些機制,來存取這些封裝起來的行為,不是自討苦吃嗎?原本就知道,這些東西很可能會一直變化,卻又去存取它,測試它,導致單元測試因此維護與異動頻率增加,這不就違背了封裝的用意?

對使用物件的角度來說,使用端根本不關心這些變化,卻因為單元測試用髒方法硬幹到這些不公開的行為,導致測試成本增加,進而導致一些不明就裡的 developer 喊出「測試很花成本,時間增加很多,很難維護」。我只想說:「這不是南北拳的問題,是你的問題。」

 

結論

說真的,剛知道 Visual Studio 2012 把 accessor 功能拿掉,我也一整個相當吃驚,覺得要強迫 developer 用 TDD 方式開發,也不用做到這麼絕吧。

但將物件導向的原則、TDD 的精神、單元測試的基本意義結合起來後,有了上述的思考歷程,就覺得只測試 public method,不建議測試 private 與 protected method,是一件正確且重要的事。

所以將這樣的思考與推論過程,分享給各位朋友參考,不一定完全符合 Visual Studio 2012 移除 accessor 的原因,這只是我自己的理解與想法而已,但從我一開始接觸單元測試,怎麼測 private method 就一直困擾我很久,雖說腦袋中有點輪廓,卻一直無法明確釐清。

 

補充

這邊有一篇寫的很不錯的文章,講的相當全面,包括概念、現實上的考量、過程中的考量,都寫得很清楚。請參考:Testing Private Methods with JUnit and SuiteRunner

2012/11/09 補充:VS2012 將 accessor 與自動產生單元測試程式碼的功能移除的另一個原因是:因為原本 accessor 的產生機制,與 MS Test 的耦合度太高了。(在 Visual Studio 2012 中,期望可以很彈性的與其他 Unit Test Provider 結合。)

 

讀者的疑問

針對讀者的一些疑問,我就補充在文末,大家若還有什麼想了解或發問的,歡迎留言。

 

Q1. 文章上只提到了 public, protected, private,那麼 internal 呢?

答: 這是一個很棒的問題,因為我文中的確沒提到 internal 的部份。

首先 internal 的定義/用意,是指在同一組件內才能看的到,也就是我這物件希望在我這組件裡是公開的,但組件外的人看不到也用不到。(這樣設計可以有效控制相依範圍)

而單元測試如前面所說,是針對「物件」的互動,來進行模擬使用。那宣告成 internal 的物件,到底要不要測試,當然要,因為的確有其他物件會使用它,我們就要思考:「怎麼使用它」。

但一般測試專案的角度來看,是參考 production code 的 library,所以對測試專案的角度,是看不到 production code 裡面宣告成 internal 的物件的,但我又想去測試 production code 中 internal 的物件,該怎麼辦?

在 .NET 中相當簡單,只需要透過:InternalsVisibleToAttribute 這個屬性設定即可。將 production code library指定給 test project可見,就可以解決這個問題。

 

Q2. 若沒針對private測試,當發生問題時,我怎麼知道是哪一段code錯了?或是它沒被涵蓋到,就代表沒有受到測試保護。

答:這個問題,就是慢慢消化這篇文章,並實際動手做之後,就會漸漸的撥開雲霧見青天了。

當只用測試的思維來看,那不去「針對」private method 測試,是一件很奇怪的事,因為它活著,但沒有測試可以馬上知道它對不對。

這也是跨入 TDD 的其中一道門檻。回過頭來看前幾篇的宗旨,系統的存在,到底為了什麼?

為了可以正確的滿足使用者的需求,外部使用的需求。既然用了物件導向來設計,既然把這些東西封裝起來,外部的使用者就根本看不到、用不到,也不該看到用到。而我們封裝的意義就在於封裝變化。這時候用其他方式,硬幹進去物件中去測試 private method,也只是增加自己未來的負擔,因為它肯定會一直變。

原本 private 的改變,可以幾乎不影響任何部分,除了物件本身內部。所以它可以放變化。

現在外面看的到這個方法,你就不能輕易改變,一旦要改,可能會影響許多測試程式,反倒是 production code 不會有太多影響。但測試程式如果因此要維護或是要重寫,這就都是根本沒必要的東西。

最後,如果你用TDD的方式開發,就根本更不會碰到這個問題。

因為,你只針對 public 行為,來進行預期,永遠切入點都是撰寫 public 的內容。大概只有重構的時候,會出現 private 跟 protected。而這個時候,被放到 private 的方法,當然是你原本放在 public 方法內的內容。

那如果原本 public 方法 code coverage 是100%,那也不會因為你搬到 private,code coverage 就變成 50%。如果出現了因為重構,就沒有涵蓋到的範圍,那就是 over design 的 bad smell,是個徵兆。

這邊就是需要搭配 TDD 與 Refactoring 的手法,才能一體成型,享受其美妙之處而無後顧之憂。

再強調一次,private / protected 的方法內容,在 TDD 裡面,基本上都是因為 refactoring 的 extract method 所產生的,都是一些原本放在 public / internal 的 function 內容。而不會是直接動手去寫 private function,除非你是 top-down 的先訂出程式的骨頭。但最終,private function 仍屬於public function 內容的一部分。

所以要特別滿足的應該是:您是否有針對外部可見的行為,進行了所有具代表性的情境來做測試。如果真的涵蓋了所有,包括 exception handling,那麼這個物件內,沒被涵蓋到的部份,基本上都可以刪除了。絕不會對外部使用造成任何影響。

對敏捷開發有興趣的朋友,可以參考我的粉絲專頁:91敏捷開發之路

對 TDD 課程有興趣的朋友,課程內容、大綱與學員心得,可以參考 skilltree 的公開課程:自動測試與 TDD 實務開發

若需要聯絡我,可以透過粉絲專頁私訊或是側欄的關於我。