Feb. 25 20:09~22:17
很多有在寫單元測試的鄉民們都會遇到一個問題:「到底要不要幫private method寫單元測試?」關於這個問題反對與贊成的人都有各自的道理。
鄉民們的看法
反對者:Private method一定會被某個public method呼叫,我們只要測試public method等於也測了private method,所以不需要針對private method寫測試。而且從資訊隱藏的角度來看,private method之所以是「private」,就表示不希望被外界的人看到。從測試的角度來看,test case也是待測程式的「外人」,所以不應該看的見private method。既然看不見,就更談不上要幫private method寫測試了。
贊成者:Private method也有程式實作邏輯,既然有邏輯就應該要被測試。雖然可以透過public method測試到private method,但是要用這種方式來涵蓋private method的各種執行路徑則測試案例將會變得很複雜,也不容易撰寫。更何況「單元測試」原本就是一種「白箱測試」,也就是測試者是在可以看到待測程式原始碼的前提之下所寫出的測試案例。從測試者可以「看到待測程式原始碼」的這個角度來看,private當然也應該測試啊,這和資訊隱藏並不違背。
JUnit Recipes怎麼說
聽起來雙方都各有各自的道理,說實話Teddy也不確定哪一種方式比較好。以前Teddy的習慣,如果private method的邏輯有點小複雜的話,是會幫private method寫單元測試的。前一陣子Teddy為了準備3月份「單元測試與持續整合實作班」的教材,重新翻閱了《JUnit Recipes》這本2005年出版的「老書」,書中提到是否須測試private method的看法,Teddy覺得寫的不錯,節錄出來給鄉民們參考。
基本上這本書是認為不要幫private method寫測試,一旦鄉民們覺得要幫private method寫測試程式,也就表示這個private method不僅僅是扮演著helper method的角色。換句話說,這個private method的邏輯可能蠻複雜的,複雜到鄉民們覺得不對它單獨測試會過意不去。因此書中認為鄉民們應該考慮將這個private method抽離出來,放到另一個class中成為public method。此外,這種作法還可以符合「Single Responsibility Principle」。
看一個例子
今天Teddy看到一個「前人」所寫例子,剛好可以用來解釋上述概念。請參考下圖,假設鄉民們要設計一個ATM(提款機程式),這個ATM程式有一個類別叫做Account,用來代表一個用戶的帳戶。這個Account有一個withdraw() method,可透過它來提款。為了防止詐騙集團,提款程式規定每天提款上限為3萬元。withdraw()有兩個參數,第一個參數是提款金額,第二個參數是提款日期。
以下是withdraw()的程式碼,它透過exceedDailyLimit()這個private method來判斷本日提款金額是否已達上限,若已達上限則不讓使用者提款,並傳回false。
接下來是exceedDailyLimit()這個private method的程式碼,它透過另一個private method:getWithdrawAmountOfDate()來得到本日已提款的總金額。
接著再繼續看getWithdrawAmountOfDate()的程式碼,這裡面的邏輯就有點複雜了,不單獨對它測試有點對不起自己的良心啊。
***
應該要重構…嗎?
看完上面這個例子,再思考一下《JUnit Recipes》書中的講法,似乎真的應該把exceedDailyLimit()與getWithdrawAmountOfDate()抽離出來放到另外一個類別中,並且變成public mehtod,這樣似乎可以減輕Account類別的責任。
不過,剛剛Teddy跟Kay討論這個問題的時候,她插了一句話。
Kay:可是有些private method只是做一些輔助的計算,只有它所屬的這個類別會用到,如果把這些private method抽離到另外一個類別,那就會出現很多小的類別,但是很可能都只包含一根手指就數的完的method個數。
是啊,OO(物件導向)技術用到某種程度,有時就是會出現數量眾多的小型類別,但好處是類別之間的互動關係與程式結構變得比較清楚,程式也比較容易維護與測試。
***
最後為了Teddy的食、衣、住、行、育、樂,忍不住置入性行銷一下。欲知更多單元測試與持續整合技巧,歡迎參加3月23~24所舉辦的「單元測試與持續整合實作班」,3月11日前報名早鳥優惠中。
***
友藏內心獨白:容易測試的程式通常設計也做得比較好。
ATM這例子...還再繼續用啊...
回覆刪除這個例子設計的這麼好,當然要一直reuse啊 XD。
回覆刪除其實當初確實有想把private methods抽出去啊,但為了讓例子好讀,還是留在Account裡了,不過,這也正好埋了一個梗XD
回覆刪除我覺得這個梗很棒耶,當例子很好。
回覆刪除我對這樣的作法有很大的顧慮.
回覆刪除為了要做單元測試而把 private 變成 public 是否太過本末倒置?
想想當初為何要宣告成 private? 通常是不想讓外部的程式去依賴它, 可能原因是因為裡面的實做日後很容易變動, 可能是想保留日後對 method signature 的可變性的掌控.
一旦變成 public, 意義上就變成 interface contract 合約的一部分, 也代表日後重構要負擔的回溯相容性的壓力! 既然是 contract, 就該審慎看待每一個被 public 的東西, 改變 visibility scope 這樣影響深遠的因素, 怎麼能僅僅為了方便測試而做異動呢?
但是對於複雜的 private function, 我主張還是要安排單元測試去驗證.
Hi Tom,
回覆刪除《JUnit Recipes》建議的作法,是要提醒開發人員,如果private method有著過於複雜的邏輯,是否表示該class負擔了太多責任?如果是,這些責任有沒有可能將其移到另一個class之中,並非單純因為『測試』就把private method變成public method。書中的觀點是從 TDD的角度出發,所以已經牽涉到設計的問題。
另外強調一下,書中提到並非直接把private變成public,而是抽離到另一個class,變成另一個class的public,這兩者還是有一點差別。
不論是在原本的 class 變成 public 或是放到另一個 class 變成 public, 對我來說就是 public, 就是變成外界可以依賴綁定的合約.
回覆刪除我看不出這樣有任何減輕對外界負責的責任義務?
難道這些被切出來的 public class & method 日後就可以隨意修改和消失移除, 而不必顧慮 consumer app or lib?
就設計觀點出發, 只是建議把責任過大的程式分攤出去給專屬的 class 來讓結構簡潔易懂, 這個設計原則我同意, 但這個原則並沒有因此要把 internal / private class 因此就變成 public.
回覆刪除單純就 public 這件事, 完全是為了測試方便, 怎麼看都跟依責任歸屬切割 class 這件事毫無關係.
對吧?
不應該說是為了“測試方便“,而是經由單元測試發現有一段code難以測試,它正暗示你design可能有問題,很可能就是Teddy說的違反了SRP.如果先寫測試程式,private method要不要測這個問題,也許就不存在了吧.
回覆刪除「有一段 code 難以測試」這問題不會跟一個 method 是 public / private 有關。不管是否 public 都會有難以測試的 code,這部分 Teddy 大哥已經有寫了一系列非常精彩的經驗文章討論,我都拜讀過非常折服。
回覆刪除會覺得 private code 難以測試的前置條件,是因為:認為必須透過 public 來間接測試到 private,所以 condition coverage 不容易做到觸發每個 private 都能夠被測試到的機會。
如果願意直接針對 private 作直接單元測試,這樣的困難根本不存在。
根據 Teddy 描述那本書的作者想法,其實是這樣的:他先主張「不應該」直接測試 private,單元測試應該「只從」 public 著手。但他也知道實務上某些 private 真的有單獨測試的必要,但為了不與他原先的主張有所抵觸,所以他建議改為 public 來自圓其說。但直接改 public 顯得手法太過粗糙,所以他引用了一個跟 design 有關的考量來遮掩這個粗糙的手法。
這是我看完文章後的感覺。
public 這件事就像是把原本內心的 OS 變成說出口的話一樣覆水難收,話說出口,要否認都很困難。
如果 OO 特性之一是強調封裝和抽象,如果做軟體不想日後修改造成太多 side-effect,那就別輕易考慮 public。
否則我們寫 getter / setter 來取代直接 public data member 不就是一個白工?
回到原主題,如果該書作者一開始就願意直接針對 private 做單元測試,根本不用為了做到測試而冒著 public 的風險。所以我說整件事情非常本末倒置。
這句話:因此書中認為鄉民們應該考慮將這個private method抽離出來,放到另一個class中『成為public method』。此外,這種作法還可以符合Single Responsibility Principle。
回覆刪除就算單純從 design 來看,SRP 原則其實並沒有包含:『成為public method』這件事,只有抽出為獨立 class 而已。
『』中的部分,算是書籍作者魚目混珠塞進去的觀念吧。所以這部分我不認同。
還請先進多多指教。
可能是我描述的有問題,我引用一下原文好了,書中第 81 頁:
回覆刪除If you want to write a test for that private method, the design may be telling you that the method does something more interesting than merely helping out the rest of that class's public interface. Whatever that helper method does, it is complex enough to warrant its 。裡面的 test, so perhaps what you really have is a method that belongs on another class--a collaborator of the first.
我個人是認為這一段話寫的還蠻有道理的...XD。
不過也不是說想測 private method就直接把它移到另一個class變成public method,還是要先思考原本放在 private method裡面的邏輯為什麼會複雜到需要單獨測試。我文章中舉的那個ATM例子,裡頭的 private method似乎就很合適移到另一個class中,因為『管理每天的交易記錄』這件事的確不太應該放在Account身上。
Teddy 大哥您的描述沒有任何問題,看過原文後,我覺得跟您文章中的中譯意思完全相同。
回覆刪除先提出一個疑問:為何 private 內不該有複雜的邏輯呢?
憑什麼作者認為只有 public 的 method 才有資格擁有複雜的內部邏輯呢?
因為作者有這樣的可見性歧視,所以才會有『只有public應該測試,private 如果需要測試,很可能他本來應該被 public 才對』這樣的論調。
到底邏輯複雜不複雜,跟 visibility scope 以任何因果關係嗎?
我個人認為是風馬牛不相干耶。
這部分還請 Teddy 兄解惑指點。
Hi Tom,
回覆刪除下此寫篇 blog 來討論一下這個問題....。
突然對 Teddy 兄有些不好意思。
回覆刪除對那書有疑問,我應該直接找書籍原作者討論,不應該在此隔山打牛,騷擾格主。
若原書作者有給我進一步回覆,再回饋於此。