[ASP.NET]重構之路系列v5 –單元測試, Just Do It!!
前言
還記得在重構第一篇[ASP.NET]重構之路系列v1 – UI, Business logic, Data access概念分開的時候,我們提到了要重構,第一步應該先建立好測試,才能確保重構的結果正確與否。沒錯,現在就是那個moment了,讓我們為我們現在的程式開始建立好我們的單元測試,沒這麼困難,跟著做就對了。
這邊要介紹的單元測試,只是第一步,也是從沒有到有最重要的一步。實際要讓整個系統單元測試run的順利,有很多不同的門檻,如果有機會,我會再開闢另一個系列是測試系列之路,屆時在那邊再針對測試的方式、策略、工具、可測試性等等進行較詳細的說明。
需求說明
這次不是可惡的PM提的問題了,請PM可以在旁邊坐著休息一下。當其他部門的同事,使用我們之前建立的Service,結果網頁出現『帳號不存在』的錯誤。原service程式碼如下:
public VerifyStatus VerifyPasswordById(string id, string password)
{
DataTable dt = MyAuthenticationDao.QueryPasswordById(id);
if (dt.Rows.Count > 0)
{
if (password == dt.Rows[0]["Password"].ToString())
{
return VerifyStatus.Passed;
}
else
{
return VerifyStatus.Failed;
}
}
else
{
return VerifyStatus.NoExist;
}
}
這時候公說公有理,婆說婆有理,大家都不認為自己的程式有錯。怎麼辦?該相信誰?
答案是:誰都不要相信,相信你的測試程式,相信被測試程式測過的程式。
設計步驟
在動手之前,先聲明一下,我這系列的文章會盡量順便帶到IDE可以幫助到我們的功能,有可能您的Visual Studio不支援這樣的功能,也沒有關係,因為沒有這樣的工具,我們也可以手動的做到一樣的效果。如果大家手上用的工具,明明就有這麼方便的功能,卻一直被埋在IDE裡面,沒有時間研究,每次都重新打造,那真的就可惜了,善用工具,也是一個Professional的表現。
步驟一:
在我們要測試的方法上(方法內任何一行都可以),按下滑鼠右鍵,選擇『建立單元測試』。
給定測試專案的名稱後,會看到該有的參考,該有的測試類別,以及測試方法需要的參數、要測試的目標、回傳的型別、預設要測試的狀況、Assert的防呆,Visual Studio都幫我們建立好了。(補充說明,當method上頭有[TestMethod()]時,這個方法才會被測試唷)
步驟二:
我們先將測試方法的名字,改成我們預計要測試的情況。接著將原本的測試程式,加上3A原則的註解來區分開,沒有特殊的原因,而是寫起來舒服,看起來爽。
3A:
- Arrange:初始化測試目標class,並將需要的屬性值建立好。初始化方法參數,初始化期望結果。(等等還會包括,初始化Stub。)
- Act:呼叫測試目標的方法,得到實際結果。
- Assert:比對實際結果,與期望結果,是否一致。
如果你跟我一樣是急性子,迫不及待的就先跑測試下去,恭喜你,你會得到下面這個結果。當然失敗囉,不過別怕,當你很熟悉單元測試的時候,你會發現,看到單元測試失敗,是一件很爽的事。都成功,才需要驚驚。單元測試失敗後,改到成功,就代表你程式的品質更上一層樓了。
看不懂錯誤訊息,沒關係,讓我們double click看一下詳細的測試報告:
double click進去後,發現:靠,這哪裡有錯!明明就沒錯啊。沒關係,眼見為憑,偵錯給它開下去。
偵錯時發現,Visual Studio是對的,我們的MyAuthenticationDao是null,的確是NullReferenceException。
稍微修改一下我們的程式,new一個Dao的instance,assign給我們的target。並將期望結果改為NoExist。
再跑一次,發現還是測試結果還是錯的。我要再強調一次,錯,是好事。我們來找原因在哪。
沒錯,這什麼鬼!!因為這是Sample Code啊…這個方法根本就沒有寫完,跟只有一行throw new NotImplementedException();是一樣的意思的。但是,我是寫Service的人啊,Dao撈資料錯了,干我屁事。沒錯!請那一位同事去找寫Dao的人,但是,我們也還沒證明我們的Service是對的,接下來就是單元測試很酷的地方了。
單元測試重要的宗旨:測試目標的方法,應該與外界類別隔離,把我們的注意力focus在我們方法的邏輯上,外面的方法錯,是他們家的事。我們要care的是,當外部類別如同預期的給我們對應的資料,我們的程式,就可以如預期的回傳我們要的結果。
測試我們的service,為什麼要連DB? 難道不能連DB,我service的測試方法失敗,就等於我的程式有錯?這比扯鈴還扯。
讓我們來證明,我們的程式是對的吧!
步驟三:
我們要跟外部class無關,首先就要手動的刻一個我們能決定回傳結果的Dao。
先來規劃一下我們要怎麼做,看一下我們的class diagram:
接下來,就讓我們來寫我們的StubDao吧,還記得我們的老招嗎?就先把code寫下去,再自動產生就好啦。
很好,到這邊,我們已經打造好我們的StubDao了,接下來,我們只要定義,QueryPasswordById要回傳什麼資料就可以了。既然我們的測試方法是要測找不到資料的情況,那我們就直接回傳一個空的DataTable即可。
接著我們偵錯看看,測試程式會怎麼走。
果然,dt.Rows.Count是0,(廢話,new DataTable()當然是0)。
噹噹噹!大功告成!我就說我的程式在Dao沒資料的時候,結果會回傳NoExist嘛!事實證明的確如此!
(同事迷之音:啊我是要問,為什麼我id給joey,密碼給79979,我DB明明就有資料,為什麼結果你的service是回傳NoExist咧?!)
步驟四:
為了讓那位毛很多的同事閉嘴,我們就根據他的描述設計我們的測試程式,id給joey,密碼給79979,預計DB會回傳一筆資料,密碼是79979,而這位同事的期望結果為Passed。
這個時候,請用我們的步驟一,直接在我們的方法上面,再按一次滑鼠右鍵,選擇『建立單元測試』,直接建立在我們已經存在的測試專案中,這樣才會快咩!
接下來,我們會發現一個問題,我的Dao該給什麼?給剛剛那一個JoeyDao,再把QueryPasswordById的回傳改成我們要的資料嗎?答案是No!!這樣我們原本測試空資料的方法不就失敗了?
難道,又要寫一個JoeyDao2,來回傳對應的資料?哇咧,這樣寫測試程式真的好花時間啊,難怪大家都說單元測試要花很多成本。
來來來,介紹你好藥,這個時候我們就要靠mock framework來輔助我們。
步驟五:
先來看我們的class diagarm如下:
這邊我範例中使用的mock framework為Rhino.Mocks(下載點),寫法很簡單,照著做就對了:
將Rhino.Mocks.dll加入測試專案參考中,記得using Rhino.Mocks。
實際的mock程式其實只有兩行:
- 透過MockRepository來產生一個實作IAuthenticationDao的stub物件。(跟我們自己定一個class實作IAuthenticationDao,再new一個instance一樣意思)。
-
定義這個stub物件
- 被呼叫哪一個方法
- 傳入哪一個參數
-
預計會回傳什麼值
(跟我們實作方法中,自己定義要回傳什麼值一樣意思)
該寫的寫完了,接著就讓我們來看實際執行的結果。可以看到,mock framework幫我們產生的stub物件型別是一串落落長的名字,其實從名字可以看出來,Rhino.Mocks使用的dynamic proxy framework是Castle DynamicProxy framework。(Dynamic Proxy也可以應用在AOP的設計,可以參考:[Spring.Net]Aop introduction–以performance log為例)
我自己是習慣會全部的測試都再跑一次,以確定這一連串的修改,不會影響到其他測試程式預期的執行結果:
OK!現在可以拿著你的測試報告,去跟那一位同事大聲的說:我的service沒有問題,如果DB回來的資料是這一筆,那我Service回傳的一定是Passed!
結論
-
這樣的單元測試,其實有個重要的前提,也就是我們在v4中,使用了IoC的方式,來設計類別與類別之間的相依性。讓類別相依於介面,而不直接相依於另一個類別。
-
可測試性,是衡量程式品質的一個重要指標。當無法測試的原因是因為無法決定外部類別回傳的值,或是外部類別無法運作,代表程式的耦合度太高,無法使用mock。當都可以使用mock來做測試,但是測試程式卻得寫得很冗長,可能代表這個待測試的方法所負責的功能太雜,進而推斷這個方法的內聚力不高,可能有調整的空間。(不過不代表這個class的內聚力不高)
-
mock framework的原理,也是dynamic proxy的一種應用,只需要簡單的兩行,我們就可以決定production code裡面用到的外部類別會回傳什麼值,來讓我們的單元測試可以跟外部完全獨立。
-
單元測試報告是保護自己的良藥,可以證明自己的程式在任何環境下(例如沒有網路、沒有與DB連線、雲端等等…),邏輯是無誤的。當整個系統不論進行了什麼修改,我們都可以快速的知道,程式是否如同原本預期般運作,避免牽一髮動全身,或是等待使用者踩到地雷引爆的情況發生。這也是為什麼,講重構一定得先有測試保護(不一定是單元測試,也可能是先用整合測試),因為要確定修改前後的執行結果一致。還有,產生bug的test case input值是相當有參考價值的,透過bug、增加單元測試、測試失敗、修改程式、測試成功,逐步的增加系統品質,讓系統更穩固。
-
請善用快速鍵,將會事半功倍。
- Ctrl+R, T:執行這個測試方法
- Ctrl+R, Ctrl+T:偵錯這個測試方法
- Ctrl+R, A:執行所有測試方法
最後再提醒大家一次,要相信誰的程式?production上的程式?版本庫裡面的程式?明星工程師寫出來的程式?大師寫出來的程式?都不是,請相信通過測試程式的程式!
Sample Code:RefactoringSample-v5.zip
blog 與課程更新內容,請前往新站位置:http://tdd.best/