在Visual Studio 2012 中,針對Unit Test 的部分,有一個重要的變動:
原本針對「測試對象非public 的部分」,開發(fā)人員可通過Visual Studio 2010 自動產(chǎn)生的accessor ??來進(jìn)行測試。但在Visual Studio 2012 中,將此功能移除了。
Accessor ??其背后的原理,是將對象通過很「臟」的反射方式,把對象內(nèi)所有的東西public 出來。并且Visual Studio 在更新對象后,進(jìn)行與設(shè)計測試時,會幫你做同步產(chǎn)生accessor ??的動作。(實際的原理我沒有深入研究,也不太確定。但基本上的概念就是如此)
這個原本被認(rèn)為很方便、實用的功能(包括我很久之前寫測試時,也是這么認(rèn)為),很抱歉,在Visual Studio 2012 后已經(jīng)被移除了。
接下來本篇文章將會說明,單元測試是否應(yīng)該對測試對象非public 的部份,進(jìn)行單元測試。
一言以蔽之:「單元測試就是用來模擬外部如何使用測試目標(biāo)對象,驗證其行為是否符合預(yù)期」。
因此,有個重點是:外部如何使用測試目標(biāo)對象。
讓我們回到Object-Oriented 的封裝原則,封裝的用意在于:
有了對單元測試與封裝的認(rèn)知后,接下來說明,為什么單元測試只需要針對測試目標(biāo)對象public 的行為,進(jìn)行測試即可。
根據(jù)單元測試的意義,以及封裝的用意,代表著「外部使用者原本就不需要了解,也根本不了解,測試目標(biāo)對象非public的行為」。單元測試既然是模擬外部使用端的動作,那當(dāng)然只針對測試目標(biāo)對象public的行為進(jìn)行模擬與驗證。
但一些朋友肯定有些疑惑,那非public 的method 該怎么辦?不測嗎?那code coverage 怎么提升?要怎么知道這些非public 的行為有沒如同預(yù)期般運作呢?
有這些疑問是正常的,因為我一開始也是有一模一樣的疑問,但開始接觸TDD 之后,反而更加了解了Unit Test 的本質(zhì)。
所謂的非public 的行為,其存在的原因,一定是因為某一些public 的行為會用到這些PRivate 或protected 的method,如果對象中存在著跟public method 無關(guān)的private 或protected method,那在設(shè)計上就是個問題,這些非public 的method 根本就沒有存在的意義。因為外部使用測試目標(biāo)對象時,完全不會用到這些method,就像聲明了變量卻不去使用它一樣,沒有意義。
而當(dāng)私有或受保護(hù)的方法與public 方法有關(guān)時,那針對公有方法的單元測試便會涵蓋到這些私有或受保護(hù)的方法,它們就是公有方法的一部分,對外部使用者來說,根本分辨不出來什么是私或受保護(hù)的,因為只關(guān)注在對象外部可視行為上。
所以,在實作單元測試上,倘若測試對象一個public method 中,涵蓋了一個private method,而private method 中與外部對象或服務(wù)相依,那么在測這個public method 時,要連private method 中相依的interface ,都要撰寫stub object 來模擬才行,這也是為什么單元測試被稱為白箱測試的原因。但還是得強調(diào)一次,外部使用者是無法分清楚哪一部分是public method 內(nèi)容,哪一部分是非public method。
總結(jié)上面的說法,非public method 的測試涵蓋率??,是依據(jù)public method 調(diào)用時的input 來決定。
有沒有可能,當(dāng)public method 該測的都測了,甚至public method 主體內(nèi)容涵蓋率都100% 了,非public 的部分涵蓋率卻很低?當(dāng)然有可能,但這要厘清一下,沒有被涵蓋到的部份,是屬于什么樣的代碼。
如果在非public method 中,沒被測試覆蓋的部份,是提醒、斷言之類的代碼,那么是屬于正常的情況。因為可能在調(diào)用非public method 之前,就已經(jīng)先提醒了,導(dǎo)致非public method 中的提醒永遠(yuǎn)不會發(fā)生。但,因為系統(tǒng)的健壯性考量,該斷言、提醒、驗證的部份,還是不能少。因為不會知道未來其他方法調(diào)用前,有沒做好提醒的部份。
那么,在private或protected method中,非提醒、斷言的代碼,卻又沒被涵蓋到部分呢?這是個警訊,代表著這些代碼可能是over design,或是根本沒有用處。因為這個對象所有對外的行為,所有的可能性,都模擬過一次了,卻都不會用到這些沒被涵蓋到的代碼,這不就代表「這些代碼目前用不到」嗎?YAGNI原則就是在說這件事:「You ain't gonna need it !」
只要public 的行為如同預(yù)期,即使private 或protected 的method 是hard-code,是很沒彈性,是很愚蠢的寫法,對外部使用來說,根本就不在乎,因為無感。
這也是TDD 所提倡的精神,如果所有使用行為都符合預(yù)期,就代表功能完成了。而且依據(jù)測試來撰寫的生產(chǎn)代碼,幾乎不會出現(xiàn)測試涵蓋不到的code,因為生產(chǎn)代碼 是為了滿足測試而撰寫的。不需要存在用不到的生產(chǎn)代碼,因此,也可以避免over design 的情況。
上面那一段的說明,肯定還是無法說服所有人,「為什么要把已經(jīng)存在的功能移除?」
不用accessor ??的人大可不用,但已經(jīng)在用,或真的得用的人,還是希望可以在VS2012 中繼續(xù)使用。
回到封裝的用意上,「封裝變化」一直是對象導(dǎo)向設(shè)計中很重要的設(shè)計原則。那些針對private與protected進(jìn)行單元測試的朋友,有沒有過「因為一些需求更新,導(dǎo)致單元測試程式就需要跟著重新調(diào)整、設(shè)計或修改,而且頻率與范圍導(dǎo)致測試的維護(hù)成本增加不少」的經(jīng)驗。如果有,這就是為什么不希望developer去針對非public method寫單元測試的原因。
著重在非public method 的單元測試,說穿了只是寫給developer 爽而已。因為要封裝變化,才會把這些內(nèi)容變成private 或protected,以期望變化時對外部使用者來說,呈現(xiàn)無感,也就是降低耦合,也就是最小知識原則。
現(xiàn)在單元測試卻通過某些機(jī)制,來存取這些封裝起來的行為,不是自討苦吃嗎?原本就知道,這些東西很可能會一直變化,卻又去存取它,測試它,導(dǎo)致單元測試因此維護(hù)與更新頻率增加,這不就違背了封裝的用意?
對使用對象的角度來說,使用端根本不關(guān)心這些變化,卻因為單元測試用臟方法硬干到這些不公開的行為,導(dǎo)致測試成本增加,進(jìn)而導(dǎo)致一些不明就里的developer喊出「測試很花成本,時間增加很多,很難維護(hù)」。我只想說:「這不是南北拳的問題,是你的問題。」
說真的,剛知道Visual Studio 2012 把accessor ??功能拿掉,我也一整個相當(dāng)吃驚,覺得要強迫developer 用TDD 方式開發(fā),也不用做到這么絕吧。
但將對象導(dǎo)向的原則、TDD 的精神、單元測試的基本意義結(jié)合起來后,有了上述的思考?xì)v程,就覺得只測試public method,不建議測試private 與protected method,是一件正確且重要的事。
所以將這樣的思考與推論過程,分享給各位朋友參考,不一定完全符合Visual Studio 2012 移除accessor ??的原因,這只是我自己的理解與想法而已,但從我一開始接觸單元測試,怎么測private method 就一直困擾我很久,雖說腦袋中有點輪廓,卻一直無法明確厘清。
這邊有一篇寫的很不錯的文章,講的相當(dāng)全面,包括概念、現(xiàn)實上的考量、過程中的考量,都寫得很清楚。請參考:Testing Private Methods with JUnit and SuiteRunner
2012/11/09 補充:VS2012 將accessor ??與自動產(chǎn)生單元測試代碼的功能移除的另一個原因是:因為原本accessor ??的產(chǎn)生機(jī)制,與MS Test 的耦合度太高了。(在Visual Studio 2012 后,期望可以很彈性的與其他Unit Test Provider 結(jié)合。)
針對讀者的一些疑問,我就補充在文末,大家若還有什么想了解或發(fā)問的,歡迎留言。
Q1. 文章上只提到了 public, protected, private,那麼 internal 呢?
答: 這是一個很棒的問題,因為我文中的確沒提到internal 的部份。
首先internal 的定義/用意,是指在同一組件內(nèi)才能看的到,也就是我這對象希望在我這組件里是公開的,但組件外的人看不到也用不到。(這樣設(shè)計可以有效控制相依范圍)
而單元測試如前面所說,是針對「對象」的互動,來進(jìn)行模擬使用。那聲明成internal 的對象,到底要不要測試,當(dāng)然要,因為的確有其他對象會使用它,我們就要思考:「怎么使用它」。
但一般測試項目的角度來看,是參考生產(chǎn)代碼 的library,所以對測試項目的角度,是看不到生產(chǎn)代碼 里面聲明成internal 的對象的,??但我又想去測試生產(chǎn)代碼 中internal 的對象,該怎么辦?
在.NET中相當(dāng)簡單,只需要通過:InternalsVisibleToAttribute這個屬性設(shè)定即可。將生產(chǎn)代碼 library指定給test project可見,就可以解決這個問題。
Q2. 若沒針對private測試,當(dāng)發(fā)生問題時,我怎么知道是哪一段code錯了?或是它沒被涵蓋到,就代表沒有受到測試保護(hù)。
答:這個問題,就是慢慢消化這篇文章,并實際動手做之后,就會漸漸的撥開云霧見青天了。
當(dāng)只用測試的思維來看,那不去「針對」private method 測試,是一件很奇怪的事,因為它活著,但沒有測試可以馬上知道它對不對。
這也是跨入TDD 的其中一道門檻。回過頭來看前幾篇的宗旨,系統(tǒng)的存在,到底為了什么?
為了可以正確的滿足使用者的需求,外部使用的需求。既然用了對象導(dǎo)向來設(shè)計,既然把這些東西封裝起來,外部的使用者就根本看不到、用不到,也不該看到用到。而我們封裝的意義就在于封裝變化。這時候用其他方式,硬干進(jìn)去對象中去測試private method,也只是增加自己未來的負(fù)擔(dān),因為它肯定會一直變。
原本private 的改變,可以幾乎不影響任何部分,除了對象本身內(nèi)部。所以它可以放變化。
現(xiàn)在外面看的到這個方法,你就不能輕易改變,一旦要改,可能會影響許多測試程式,反倒是生產(chǎn)代碼 不會有太多影響。但測試程式如果因此要維護(hù)或是要重寫,這就都是根本沒必要的東西。
最后,如??果你用TDD的方式開發(fā),就根本更不會碰到這個問題。
因為,你只針對public 行為,來進(jìn)行預(yù)期,永遠(yuǎn)切入點都是撰寫public 的內(nèi)容。大概只有重構(gòu)的時候,會出現(xiàn)private 跟protected。而這個時候,被放到private 的方法,當(dāng)然是你原本放在public 方法內(nèi)的內(nèi)容。
那如果原本public 方法code coverage 是100%,那也不會因為你搬到private,code coverage 就變成50%。如果出現(xiàn)了因為重構(gòu),就沒有涵蓋到的范圍,那就是over design 的bad smell,是個征兆。
這邊就是需要搭配TDD 與Refactoring 的手法,才能一體成型,享受其美妙之處而無后顧之憂。再強調(diào)一次,private / protected的方法內(nèi)容,在TDD里面,基本上都是因為refactoring的extract method所產(chǎn)生的,都是一些原本放在public / internal的function內(nèi)容。而不會是直接動手去寫private function,除非你是top-down的先訂出程序的框架。但最終,private function仍屬于public function內(nèi)容的一部分。
所以要特別滿足的應(yīng)該是:您是否有針對外部可見的行為,進(jìn)行了所有具代表性的情境來做測試。如果真的涵蓋了所有,包括exception handling,那么這個對象內(nèi),沒被涵蓋到的部份,基本上都可以刪除了。絕不會對外部使用造成任何影響。
備注:這個系列是我畢業(yè)后時隔一年重新開始進(jìn)入開發(fā)行業(yè)后對大拿們的博文摘要整理進(jìn)行學(xué)習(xí)對自我的各個欠缺的方面進(jìn)行充電記錄博客的過程,非原創(chuàng),特此感謝91 等前輩
新聞熱點
疑難解答