防御式編程是保護自己的程序不受外部侵害的一種有效方式。代碼大全在“防御式編程”這一章中講到,可以用斷言(assert)來檢查真實情況是否滿足自己的預期,例如指針非空。 上個月 Miko Hevery 寫了一篇文章: 斷言,還是不斷言 ,描述了在" name="description" />
防御式編程是保護自己的程序不受外部侵害的一種有效方式。代碼大全在“防御式編程”這一章中講到,可以用斷言(assert)來檢查真實情況是否滿足自己的預期,例如指針非空。
上個月Miško Hevery寫了一篇文章:斷言,還是不斷言,描述了在構造器中編寫斷言給單元測試所造成的種種不便。
他先舉了一個實際例子:
class House { Door door; Window window; Roof roof; Kitchen kitchen; LivingRoom livingRoom; BedRoom bedRoom; House(Door door, Window window, Roof roof, Kitchen kitchen, LivingRoom livingRoom, BedRoom bedRoom){ this.door = Assert.notNull(door); this.window = Assert.notNull(window); this.roof = Assert.notNull(roof); this.kitchen = Assert.notNull(kitchen); this.livingRoom = Assert.notNull(livingRoom); this.bedRoom = Assert.notNull(bedRoom); } void secure() { door.lock(); window.close(); } }
然后說到,因為secure方法只需要door和window兩個對象,所以理所當然的是,在測試這個方法的時候只需要初始化door和window,其他參數用null代替,如: 、
House house = new House(door, window,null, null, null, null);
這樣的方法才能夠讓人看明白意圖,但是因為構造器中做了參數非空的斷言,所以就不得不這樣初始化House對象:
House house = new House(door, window,
new Roof(),
new Kitchen(),
new LivingRoom(),
new BedRoom());
這樣就讓人不太能看得懂到底是哪些對象真正在測試方法中被用到了。而當構造器中的參數增多,代碼的可讀性就會更差。Miško最后提到:
我不是反對斷言,我也在自己的代碼中也常用,不過我的大多數斷言只是用來檢查對象的內部狀態,而不是是不是傳入了一個null值。檢查是不是null往往會影響到代碼的測試,如果要我在完好測試過的代碼和有斷言但是沒測試的代碼之間做選擇,結果是不言而喻的。
有不少人跟帖反對Miško的觀點,他們認為這根本不是斷言的問題,而是來自于設計本身,例如
你為啥不用mock框架而非要自己初始化那么一堆東西呢?在.NET世界里面有個新詞,叫做automocker——你可以把它看作是mock框架和ioc容器的混合體……
看看這個鏈接吧:http://blog.eleutian.com/CommentView,guid,762249da-e25a-4503-8f20-c6d59b1a69bc.aspx
Howie則說:
不管是我寫的代碼,還是我看到的代碼,都是在調用對象的時候去做斷言,而不是在保存對象的時候。這樣合理不?所以你應該在 secure()方法里面寫Assert.notNull(door);Assert.notNull(window)。用到的時候再去檢查,而不是提前 就把一切都檢查好,以備過幾天以后需要用。
zdsbs提出用Builder來取代構造器:
Door door = new Door();
Window window = new Window();
House house = HouseBuilder.new().with(door).with(window);
house.secure();
assertTrue(door.isLocked());
assertTrue(window.isClosed());
這樣問題就很好的解決了,其他變量都可以在HouseBuilder里面提供一些缺省值,你也不會創建出具有非法狀態的對象來。
earlNameless對Miško的設計思路直接抨擊說:
我不同意你的看法,因為你這個例子里面,人們得先知道你所測試的方法只用到了door和window,沒用其他任何東西。
所以你的測試人員就得了解方法的具體實現,而這是不應該的……我如果有個可以滑開的房頂,我就得還給它上鎖,可現在房頂是null。
后面有幾個人同意earlNameLess的看法:測試代碼需要清楚的了解實現代碼的內部實現方式實在不是什么好做法。建議根據具體情況提取出接口,或者用一下Ioc容器。
不過也有些人對earlNameless的看法表示反對,例如 Adam Tybor說到:
為了寫測試,不就是得知道實現方式么。這是TDD,不是TAD,不是么?如果房頂還需要加鎖,那就讓測試失敗,然后改實現,直到所有測試變綠。
上面有很多討論都是針對Misko的測試代碼不得不知道實現細節這個做法的。雖然,測試代碼了解內部細節是有點不太穩定,但這是單元測試,單元測試是白盒 測試,不是黑盒。如果你用mock,那你肯定得知道內部細節……再說的清楚一點,你是在測試基于給定的條件,你的代碼能夠得到預期的結果。你必須得知道這 些條件是啥,把那些不滿足的條件排除掉。
如果有人來改了實現細節,那測試就會失敗。不過這也暴露出一個問題來,你必須得有一整套單元測試可以做回顧。如果我改了行為,我也就修改了測試的意圖。然后我就得修改測試,讓它對新的假設起作用。
所以這要看你是想測試行為還是輸入輸出,這已經是設計測試的哲學范疇了……
跑題跑了一陣子以后,rnaufal回到原來話題上,直接拋出了JDK文檔:
JDK文檔中有一篇的標題是“用斷言編程”,鏈接在這里:“http://java.sun.com/j2se/1.4.2/docs/guide/lang/assert.html#usage”
里面寫著:不要在public方法中用斷言做變量檢查。
讀者朋友,你在自己的代碼中使用防御式編程么?用過assert來檢查null與否么?你的單元測試是在檢查行為,還是檢查輸入輸出呢?歡迎分享你們的意見。