首先來看下面幾個場景你是否熟悉
1、你正在開發一個系統,你不斷地編碼-編譯-調試-編碼-編譯-調試……終於,你負責的功能模塊從上到下全部完成且編譯通過!你長出一口氣,懷著激動而又忐忑的心情點擊界面上的按鈕,頓時你剛剛的輕松感煙消雲散:系統無法正常工作,你想讀的數據顯示不出來,你想存的東西也送不到數據庫……於是,你再次回到IDE裡,設斷點、調試、一層一層跟蹤,當你精疲力盡終於將數據送到數據庫裡,你又發現了其它問題,於是你繼續設斷點、調試、編譯、調試……
2、你狂躁地敲擊著鍵盤和鼠標,咒罵著不斷出現的bug:啊?這裡怎麼沒返回值啊!哎?這裡不該是0啊!不對啊,這裡怎麼沒數據……你永遠不知道還有多少 bug,你也永遠不知道你的改動會不會引入其它bug——這裡有幾十個甚至上百個類,幾百幾千個方法!我不能都照顧到啊!你感覺bugs像敲擊鼹鼠游戲中的鼹鼠:打下了這個,另一個又從其它洞口露出頭來……
3、也許是畢業答辯的演示,也許是客戶的審查,你小心地打開自己要演示的系統,進行著預定的操作,忽然,有個功能不能正常運行,你大汗淋漓,在答辯老師或者客戶質疑且不滿的目光下你試了又試,但還是於事無補……於是,答辯老師可能扭頭便走,客戶可能憤然離去,然後離去的還有你的學位證和項目獎金。當後來你檢查代碼時,發現這一切竟然只是因為一個底層工具類中一個方法輸出結果為空。
如果你覺得上面的場景令你似曾相識甚至痛心疾首,那麼你應該看完這篇文章。
什麼是單元測試
單元測試(Unit Test)的一個測試用例(Test Case)是一小段代碼,這段代碼用於測試一個小的程序功能(一般是一個方法或相關的幾個方法)行為是否正常。下面給出一個實際項目中單元測試用例的代碼,大家可以不用深究這段代碼中的細節,這裡貼這段代碼只是給大家一個直觀的感覺。
1 /// <summary>
2 /// 測試基本的添加及刪除角色是否正確
3 /// </summary>
4 [Test]
5 public void TestAddAndRemoveRole()
6 {
7 IRoleServices roleServ = UnityHelper.CreateContainer().Resolve<IRoleServices>();
8 IRoleRepository roleRep = UnityHelper.CreateContainer().Resolve<IRoleRepository>();
9 Assert.IsNotNull(roleServ);
10 Assert.IsNotNull(roleRep);
11
12 String timeStamp = DateTime.Now.ToString();
13 RoleDto newRole = new RoleDto()
14 {
15 Name = "測試角色" + timeStamp,
16 Desciption = "此角色僅供測試使用",
17 };
18 roleServ.AddRole(newRole);
19
20 RoleDto addedRole = roleRep.GetRoleByName("測試角色" + timeStamp);
21 Assert.AreNotEqual(-1, addedRole.ID);//確認新角色添加成功
22
23 roleServ.RemoveRole(addedRole.ID);
24 Assert.AreEqual(-1, roleRep.GetRoleByName("測試角色" + timeStamp).ID);//確認剛才添加的角色刪除成功
25 }
上面的Unit Test Case來自我目前負責的一個項目,這段代碼的作用是測試Services層對角色的添加和刪除是否正常工作。這裡大家可以先不必太細究代碼,後面會有詳解。
為什麼大家不使用單元測試
按照慣例,說完什麼是單元測試,就該說為什麼要使用單元測試了。但是,我在這裡想先和大家討論,為什麼很多開發人員知道單元測試,也“認為”單元測試有必要,但絕大多數開發人員都不寫單元測試,能認真對待單元測試的開發人員更是寥寥無幾了。
我私下調查了一些開發人員,發現大家不寫單元測試主要有兩點原因:一是對單元測試存在很多誤解,二是沒有真正意識到單元測試的收益。下面我就這兩點做一些討論。
首先,我們來看看大家對單元測試普遍存在哪些誤解。
誤解1:單元測試屬於測試工作,應該由測試人員來完成,所以單元測試不屬於開發人員的職責范圍。
正解1:單元測試雖然叫做“測試”,但實際屬於開發范疇,應該由開發人員來做。
在大多數開發人員眼裡,“開發”和“測試”是兩個泾渭分明的范疇,他們認為:開發人員的工作就是寫新代碼,實現新功能,至於代碼的測試,那是測試人員的職責,我只要讓代碼編譯通過就行了。
我們都知道,軟件是很復雜很抽象的東西,軟件開發人員壓力都很大,況且人非聖賢,強求開發人員開發出沒有缺陷的程序是不現實的,所以才有了“測試工程師”這一職位。但是,開發人員至少應該保證一點:你寫的每一個函數或方法(Function)應該能夠正常完成功能,即行為正常。軟件最終可能會有缺陷,這不是開發人員完全可以控制的,但你寫了一個類,類裡有4個方法,作為開發人員應該保證這四個方法實現了“眼下”的功能。例如,你寫了一個獲取IoC容器的工具類,你總要保證其中的GetContainer方法能正確返回一個Container吧。
所以,單元測試雖然叫“測試”,但實際其屬於開發范疇,其目的是保證開發的功能子項能完成正確實現其基本功能。甚至我個人認為,當開發人員開發每一個功能子項(通常是方法)時,如果不能附帶一配套的單元測試代碼,都不能算開發完成。換言之,單元測試代碼應該是開發人員必須提供的要素。
誤解2:單元測試是一種測試,其功能是對代碼進行檢測。
正解2:單元測試是一種工具,其功能除了是對代碼進行檢測,更重要的是對軟件的質量起到一種保證,並且是為他人和後續編碼、重構工作提供的一種十分美妙的工具!
單元測試不是一種測試。沒錯,我不是在說瘋話,單元測試其實是一種工具。特別是當自動化測試軟件(如NUnit、JUnit)出現後,單元測試更像是一種工具了。
當你處在一個多人開發團隊中,你需要和其他隊友配合開發,而這在程序層面則表現為你開發的Class會被別人用,而你也會用別人開發的Class。我們每個人都希望別人交給我的Class是行為正確的,如果我拿到一個同事寫的數據庫操作類DBHelper,但發現其中的Connect方法根本無法連接上數據庫(雖然沒有編譯錯誤),那我將非常郁悶。所以,在交給別人一個Class之前,你應該使用Unit Test保證這個Class是正常實現功能的,在交付的時候,你應該一手遞上剛出爐的Class,然後另一只手遞上配套的Unit Test,然後說:嘿!哥們,這是你要的類,而這個是配套的單元測試,你可以隨時使用自動化測試工具運行它以便迅速知道這個類是否工作正常。
這將會是個很棒的工具,你的隊友以後可能會想知道它的改動是否影響了你提供的類的功能,也可能會對你的類進行重構,但無論何時,它只要拿出你的配套單元測試,讓自動化測試工具跑一下,不出幾秒,就知道你提供的類是否還正常完成功能。即使是對於你自己,以後也會有很多機會用到它。而當你寫的代碼出現bug,你可以拿出你這段代碼調用的所有類的單元測試跑一遍,很快就能知道到底是你依賴的類出了問題還是你自己代碼有問題,而不必抓狂似地到處設斷點。
誤解3:項目經理或技術主管沒有要求寫單元測試,所以不用寫。
正解3:寫單元測試應該成為開發人員的一種本能,開發本身就應該包含些單元測試。
就像項目經理不用告訴你要使用計算機寫程序一樣,寫單元測試應該成為開發人員的必須動作。因為你是開發人員,因為你在做開發,所以你必須寫單元測試,就這麼簡單。
誤解4:寫單元測試獲益者是測試人員,而開發人員無法從中獲益,還要搭上寶貴的時間。
正解4:寫單元測試誰都獲得不了像開發人員獲得的那麼大的益處。
有了單元測試,你可以隨時從同事手中接過值得信賴的代碼;有了單元測試,你可以隨時保證你寫的代碼行為正確;有了單元測試,你可以隨時通過自動化操作得知某個Class行為是否正確;有了單元測試,你以後的Debug和重構工作將變得輕松異常;有了單元測試,……沒有人比開發人員從中獲得的利益更大了。
為什麼開發人員很難意識到單元測試的收益
關於這點,我認為有兩個重要原因。
第一、絕大多數開發人員沒有嘗試過貫徹單元測試。
這個很好理解,如果你不親口嘗嘗一道菜,即使是海參鮑魚,你也不知道它有多美味。我曾經也是其中的一員,但當我第一次將單元測試貫徹於項目中並嘗到甜頭後,我就愛上“她”了,所以,邁出一地步,很關鍵。
第二、人有一種天性:相比長遠的更大利益,人們更傾向於眼前的小的多的利益,正所謂“貪小便宜吃大虧”是也。
想起了美國人類行為專家的一個實驗:他到了美國一個小學,裡面一個一年級班級有48個孩子,他給每個孩子5顆做了特殊標記的糖,並告訴他們如果到一周後誰能一顆都不吃,我就給他100顆糖。一周後,48個孩子中只有4個孩子做到了。他跟蹤了這48個孩子30年的成長,最後發現那4個孩子都成為了十分成功的人物,他們4個人30年後擁有的財富是剩下44個孩子財富總和的3倍。
同樣道理,即使很多開發人員也知道好的單元測試能讓以後省不少心,但他們也寧可省掉寫單元測試時間去堆砌代碼。因為我們總覺得今天省掉1個小時多寫一個類更有的賺,雖然我們以後要為省掉的1小時多付出3個小時去抓狂。
小結一下
上面寫了很多,所以我認為這裡有必要小結一下,整理一下思緒。
單元測試的概念——一小段代碼,用於檢查一個或幾個相關的方法行為是否正確。
單元測試的本質——隨功能代碼一起提供的一個配套工具。
單元測試的用途——保證交付Class行為正確,隨時可用於自動化檢測其對應的Class行為是否正確,對整個軟件的質量是一種保證,對缺陷是一種控制。
為什麼需要單元測試
我忽然發現,寫了上面的文字後,再來討論這個問題有些多余了,那麼我盡量寫簡短一點。
1、開發人員有義務提供行為正確的Class,也有權利得到行為正確的Class。
很明顯,如果你和你的同事,都能重視單元測試的話,你將同時履行這份義務和享受這份權利。
2、盡早消滅缺陷。
缺陷越早消滅所付出的代價越小,而越往後其代價呈指數增長,這是有充分的實驗數據證明的,並已經被寫到每一本軟件工程教科書中。毫無疑問,當你交付一個Class前,就將其行為上的缺陷全部扼殺,那將取得巨大的收益。
3、使合作變得愉快順暢。
想想看,每個你調用的Class,都是經過你的同事測試,確保行為是正確的,這是多麼美妙的事情!我們寫程序經常沒有安全感,我們戰戰兢兢,很大程度上是因為我們沒信心認為調用的每個Class行為是正確的。
4、得到一個有力的工具,會在後續工作中大顯身手的工具。
如果每個Class都有配套的單元測試,好的,如果你想確認你的改動有沒有影響到其它幾個Class,run it!如果你想看看你調用的類是否行為正確,run it!如果你在重構,想看看重構有沒有改變或損害其行為,run it!你正在調試一個bug但很難定位問題出在哪個地方,run it!你想看看目前項目中所有集成進來的代碼是否行為都正確 run it! ……
.NET平台下使用NUnit進行單元測試的實例
如果你願意,你可以手工設計和運行單元測試,但這是低效和讓人恐懼的。目前,各個平台上都有較為流行的自動化單元測試工具,像Visual Studio 2008本身就集成了單元測試功能。但是,我更願和大家分享的是一個叫NUnit的工具。其官方網站為:http://www.nunit.org/。這是一個開源且免費的.NET平台下自動化單元測試工具,可以在其官網下載。NUnit是XUnit家族的一員,其體積小巧,使用簡單,但功能強大,一直是我做單元測試的首選。
另外,這個例子我選取目前我正負責的一個實際項目,這是一個國家863項目。這個項目使用了敏捷開發方法,貫徹了以保證為目的的測試驅動、持續集成等實踐。我將截取其中幾個片段和大家分享一下單元測試的一些實踐。
值得一提的是,我對這個項目的單元測試要求是相對嚴格的,我們使用的配置管理工具是SVN,作為項目負責人,我對所有開發人員有一個要求:每一個新開發的Class,必須有配套的單元測試,並且在每次Commit到SVN前,不僅僅要保證Commit的代碼編譯沒問題,還要跑通所有單元測試,否則不准Commit到SVN。這就保證了每個人Update到的Class都是行為正確的。再配合面向接口編程方法和Mock技術,大大提高了代碼的可測試性,使得開發過程一直比較讓人滿意。剛開始大家覺得我的要求有些過分,但是當每次結合時幾乎都沒有出現問題,每次剛剛集成的新功能都能順利在UI上跑通,大家也就慢慢接受了,並且漸漸都對待單元測試非常認真。
這個項目的整體結構大家可以先看一下,其中XUnit項目就是單元測試項目。
圖1、解決方案結構圖
其中的單元測試Case進行了一定的組織,相應工程的Case放在了不同目錄下。當然,這個大家可以根據具體情況自行確定組織方式。
對UnityHelper的單元測試
這個項目選用的依賴注入工具為Unity(http://www.codeplex.com/unity)。因為獲取UnityContainer是一個常用操作,所以我將其封裝成一個輔助類,代碼如下:
1 /**********************************************************************
2 *
3 * 北京航空航天大學計算機學院軟件工程研究所 All Rights Reservd
4 *
5 * 任何拷貝都不允許刪除此處版權聲明
6 *
7 * 作者:張洋
8 *
9 * 建立時間:2010-01-08
10 *
11 * ********************************************************************/
12
13 using System.Configuration;
14
15 using Microsoft.Practices.Unity;
16 using Microsoft.Practices.Unity.Configuration;
17
18 namespace SPMS.Common.Utils
19 {
20 /// <summary>
21 /// 工具類
22 /// 封裝了Unity中Container的創建工作,並保證Unity Container的單例性
23 /// </summary>
24 public class UnityHelper
25 {
26 private static IUnityContainer _container = null;
27
28 /// <summary>
29 /// 獲取Unity Container
30 /// </summary>
31 /// <returns>全局唯一的Unity Container實例</returns>
32 public static IUnityContainer CreateContainer()
33 {
34 if (null == _container)
35 {
36 _container = new UnityContainer();
37
38 //從配置文件中讀取IoC配置信息
39 ExeConfigurationFileMap map = new ExeConfigurationFileMap();
40 map.ExeConfigFilename = "unity.cfg.xml";
41 Configuration config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);
42
43 //通過配置信息初始化Container
44 UnityConfigurationSection section = config.GetSection("unity") as UnityConfigurationSection;
45 section.Containers["defaultContainer"].Configure(_container);
46 }
47
48 return _container;
49 }
50 }
51 }
這段代碼只有一個方法,就是CreateContainer,其作用是獲取全局唯一的UnityContainer實例。在寫完這個代碼後,我開始寫單元測試,我能想到有四個點要測:
1)能正確返回UnityContainer
2)返回的UnityContainer能正確創建對象
3)保證創建的UnityContainer是單例的,即全局唯一實例
4)返回的UnityContainer在創建配置為單例的對象時,返回的對象應該是單例的
有了這四點想法,我寫了如下的單元測試:
1 /**********************************************************************
2 *
3 * 北京航空航天大學計算機學院軟件工程研究所 All Rights Reservd
4 *
5 * 任何拷貝都不允許刪除此處版權聲明
6 *
7 * 作者:張洋
8 *
9 * 建立時間:2010-01-08
10 *
11 * ********************************************************************/
12
13 using NUnit.Framework;
14
15 using SPMS.Common.Utils;
16 using SPMS.Repository.IRepository;
17 using SPMS.Services.IServices;
18
19 namespace SPMS.XUnit.CommonTests
20 {
21 /// <summary>
22 /// UnityHelper的單元測試類
23 /// </summary>
24 [TestFixture]
25 public class UnityHelperTests
26 {
27 /// <summary>
28 /// 測試獲取Container是否正常
29 /// </summary>
30 [Test]
31 public void TestCreateUnityContainer()
32 {
33 Assert.IsNotNull(UnityHelper.CreateContainer());
34 Assert.IsInstanceOf(typeof(Microsoft.Practices.Unity.IUnityContainer), UnityHelper.CreateContainer());
35 }
36
37 /// <summary>
38 /// 測試Container創建對象是否正常
39 /// </summary>
40 [Test]
41 public void TestCreateObject()
42 {
43 IRoleRepository roleRepository = UnityHelper.CreateContainer().Resolve<IRoleRepository>();
44 Assert.IsNotNull(roleRepository);
45 Assert.IsInstanceOf(typeof(SPMS.Repository.NHibernateRepository.NHRoleRepository), roleRepository);
46
47 IRoleServices roleServ = UnityHelper.CreateContainer().Resolve<IRoleServices>();
48 Assert.IsNotNull(roleServ);
49 Assert.IsInstanceOf(typeof(SPMS.Services.ServicesImpls.RoleServicesImpl), roleServ);
50 }
51
52 /// <summary>
53 /// 測試Container是否是單例對象
54 /// </summary>
55 [Test]
56 public void TestSingletonContainer()
57 {
58 Assert.AreSame(UnityHelper.CreateContainer(), UnityHelper.CreateContainer());
59 }
60
61 /// <summary>
62 /// 測試指定為Singleton的實例,是否為單例對象
63 /// </summary>
64 [Test]
65 public void TestSingletonObject()
66 {
67 Assert.AreSame(UnityHelper.CreateContainer().Resolve<IRoleRepository>(), UnityHelper.CreateContainer().Resolve<IRoleRepository>());
68 }
69 }
70 }
即使你沒用過NUnit,我想這段代碼也是非常好理解的。限於篇幅,不能詳細介紹NUnit,這裡只簡要說一下。使用NUnit首先要添加對 nunit.framework.dll的引用,然後引入NUnit.Framework命名空間,最後,每個測試類添加 [TestFixture]Attribute,而每個測試方法添加[Test]Attribute,這樣就可以在裡面寫測試代碼了。
其中用的最多的是NUnit.Framework.Assert類,它有很多靜態方法用於斷言,這些斷言就是你期望的行為。例如,Assert.AreSame方法斷言兩個變量是否引用同一個對象,我在上面代碼裡使用這個方法斷言UnityContainer對象的單例性。
完成這個單元測試代碼後,要把測試需要的配置文件等添加到XUnit工程裡,我這裡包括一個unity.cfg.xml,作為Unity的配置文件。下面,編譯這個工程。如果編譯沒有錯誤,下面就可以跑這個測試了。怎麼跑呢,當你安裝NUnit時,會同時安裝一個NUnit GUI,在開始菜單中找到打開,界面大約是這樣子:
圖2、NUnit GUI
選擇菜單欄的 file -> open project ,打開剛才編譯好的SPMS.XUnit.dll,也就是測試工程的dll文件,GUI會自動加載所有測試用例,如下圖所示。
圖3、加載工程後的NUnit GUI
OK,我們要測試的是UnityHelperTests下的所有測試用例,所以我們在左邊選中它,然後單擊“run”按鈕!這樣GUI就會自動幫我們跑裡面的測試用例了,最終結果如下。
圖4、成功的測試結果
可以看到,所有UnityHelperTests的測試均為綠色,進度條也為綠色,且指示Passed: 4,這說明我們所有斷言成功,測試通過。
下面我們做點手腳,我在UnityHelper代碼中取消了獲取Container的單例性,現在再來運行測試看結果:
圖5、失敗的測試結果
可以看到,創建Container及創建Object是正常的,但Container的單例性被破壞。Container都不是單例的了,兩個Object更不會是單例了。這時根據結果和右側給出的錯誤信息,我們可以很快定位缺陷,並將之排除。
這個單元測試一但寫好,開發團隊中任何人員可以隨時方便地跑它,這樣我們就能在任何時刻知道UnityHelper是否工作正常,以幫助我們定位bug和排除缺陷。如果我們要做UnityHelper的重構,我們也可以使用它保證UnityHelper的行為正確。
總結
本文首先討論了什麼是單元測試,然後討論了開發人員對單元測試的誤解以及不願做單元測試的原因。接著,我們討論單元測試有哪些作用,最後用一個實際項目中的片段來說明單元測試的實踐。限於篇幅,不能將單元測試及NUnit工具的方方面面討論詳盡,但是NUnit真是一個非常好上手的工具,你可以參考其文檔和示例,或者參看Andrew Hunt所著的《Pragmatic Unit Testing in C# with NUnit》一書。
不論你是做何種開發,我相信,單元測試一定會讓你受益匪淺。請相信,單元測試不是一件索然無味的工作,它同樣充滿了成就感和樂趣,每次看到鮮亮的綠色進度條,都是最爽的時刻。所以,希望看完本文的朋友能盡快拿起NUnit,開始你的單元測試實踐。就從你的下一個項目、或下一個Class、甚至下一個 Function,開始你的單元測試之旅吧。