iT邦幫忙

DAY 7
8

如何提升系統設計品質 - 技術與工具以.NET為例系列 第 7

[如何提升系統品質-Day7]測試-單元測試, Just Do It!!

還記得在重構第一篇[如何提升系統品質-Day2]重構– UI, Business logic, Data access概念分開的時候,我們提到了要重構,第一步應該先建立好測試,才能確保重構的結果正確與否。沒錯,現在就是那個moment了,讓我們為我們現在的程式開始建立好我們的單元測試,沒這麼困難,跟著做就對了。

這邊要介紹的單元測試,只是第一步,也是從沒有到有最重要的一步。實際要讓整個系統單元測試run的順利,有很多不同的門檻,後面的文章有機會會再介紹到。

[如何提升系統品質]系列文章連結
需求說明
這次不是可惡的PM提的問題了,請PM可以在旁邊坐著休息一下。當其他部門的同事,使用我們之前建立的Service,結果網頁出現『帳號不存在』的錯誤。原service程式碼如下:

這時候公說公有理,婆說婆有理,大家都不認為自己的程式有錯。怎麼辦?該相信誰?

答案是:誰都不要相信,相信你的測試程式,相信被測試程式測過的程式。

設計步驟
在動手之前,先聲明一下,我這系列的文章會盡量順便帶到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程式其實只有兩行:
1.透過MockRepository來產生一個實作IAuthenticationDao的stub物件。(跟我們自己定一個class實作IAuthenticationDao,再new一個instance一樣意思)。
2.定義這個stub物件
(a)被呼叫哪一個方法
(b)傳入哪一個參數
(c)預計會回傳什麼值(跟我們實作方法中,自己定義要回傳什麼值一樣意思)

該寫的寫完了,接著就讓我們來看實際執行的結果。可以看到,mock framework幫我們產生的stub物件型別是一串落落長的名字,其實從名字可以看出來,Rhino.Mocks使用的dynamic proxy framework是Cas­tle Dynam­icProxy frame­work




我自己是習慣會全部的測試都再跑一次,以確定這一連串的修改,不會影響到其他測試程式預期的執行結果:

OK!現在可以拿著你的測試報告,去跟那一位同事大聲的說:我的service沒有問題,如果DB回來的資料是這一筆,那我Service回傳的一定是Passed!

結論
1.這樣的單元測試,其實有個重要的前提,也就是我們在上篇文章中,使用了IoC的方式,來設計類別與類別之間的相依性。讓類別相依於介面,而不直接相依於另一個類別。

2.可測試性,是衡量程式品質的一個重要指標。當無法測試的原因是因為無法決定外部類別回傳的值,或是外部類別無法運作,代表程式的耦合度太高,無法使用mock。當都可以使用mock來做測試,但是測試程式卻得寫得很冗長,可能代表這個待測試的方法所負責的功能太雜,進而推斷這個方法的內聚力不高,可能有調整的空間。(不過不代表這個class的內聚力不高)

3.mock framework的原理,也是dynamic proxy的一種應用,只需要簡單的兩行,我們就可以決定production code裡面用到的外部類別會回傳什麼值,來讓我們的單元測試可以跟外部完全獨立。

4.單元測試報告是保護自己的良藥,可以證明自己的程式在任何環境下(例如沒有網路、沒有與DB連線、雲端等等…),邏輯是無誤的。當整個系統不論進行了什麼修改,我們都可以快速的知道,程式是否如同原本預期般運作,避免牽一髮動全身,或是等待使用者踩到地雷引爆的情況發生。這也是為什麼,講重構一定得先有測試保護(不一定是單元測試,也可能是先用整合測試),因為要確定修改前後的執行結果一致。還有,產生bug的test case input值是相當有參考價值的,透過bug、增加單元測試、測試失敗、修改程式、測試成功,逐步的增加系統品質,讓系統更穩固。

最後再提醒大家一次,要相信誰的程式?production上的程式?版本庫裡面的程式?明星工程師寫出來的程式?大師寫出來的程式?都不是,請相信通過測試程式的程式


上一篇
[如何提升系統品質-Day6]重構-簡單使用interface之『你也會IoC』
下一篇
[如何提升系統品質-Day8]重構-抽象來看程式是否符合DRY原則
系列文
如何提升系統設計品質 - 技術與工具以.NET為例30
0
chiounan
iT邦研究生 1 級 ‧ 2011-10-17 10:15:31

筆記很喜歡您的分享,超認真的,很有收穫。

就是91 iT邦研究生 4 級 ‧ 2011-10-17 19:57:36 檢舉

謝謝您的讚美,讓我感到欣慰很多。灑花
其實有一些內容因為篇幅限制的關係,只好刪刪減減,如果有看到不通順的地方,也請提出討論跟多海涵一下了。

0
Ken(Bigcandy)
iT邦大師 1 級 ‧ 2011-10-17 20:11:25

雖然不懂程式設計,至少我看得出來你不是隨便寫的,超認真+1

我一定推....認真的好漢!

鐵殼心 iT邦高手 1 級 ‧ 2011-10-17 20:21:34 檢舉

1++

就是91 iT邦研究生 4 級 ‧ 2011-10-17 21:22:52 檢舉

謝謝兩位 :)

0
shuan0114
iT邦好手 1 級 ‧ 2011-10-18 15:06:02

暈
厲害...讓那位毛很多的同事閉上鳥嘴!!推+1

我要留言

立即登入留言