TDD學習筆記【三】---是否需針對非public方法進行測試?

Halower發表於2014-09-01

前言

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

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

Accessor ​​其背後的原理,是將物件通過很「髒」的反射方式,把物件內所有的東西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,就像宣告瞭變數卻不去使用它一樣,沒有意義。

而當私有或受保護的方法與public 方法有關時,那針對公有方法的單元測試便會涵蓋到這些私有或受保護的方法,它們就是公有方法的一部分,對外部使用者來說,根本分辨不出來什麼是私或受保護的,因為只關注在物件外部可視行為上。

所以,在實作單元測試上,倘若測試物件一個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 所提倡的精神,如果所有使用行為都符合預期,就代表功能完成了。而且依據測試來撰寫的生產程式碼,幾乎不會出現測試涵蓋不到的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 的物件,到底要不要測試,當然要,因為的確有其他物件會使用它,我們就要思考:「怎麼使用它」。

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

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

 

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

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

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

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

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

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

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

最後,如​​果你用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 等前輩

 

 

相關文章