背景 ?
本文接《TDD(測(cè)試驅(qū)動(dòng)開發(fā))項(xiàng)目實(shí)踐》開始指孤,前文偏重設(shè)計(jì)甥雕,對(duì)測(cè)試晶乔,尤其是單元測(cè)試
涉及不多几迄,本文是對(duì)前文的補(bǔ)充,相同的項(xiàng)目案例青柄,側(cè)重的是單元測(cè)試伐债。
有關(guān)需求及項(xiàng)目描述,可以直接參看前文:
http://www.reibang.com/p/ae34612e1eeb
環(huán)境(操作系統(tǒng)&&工具&&框架&&第三方庫(kù))
① 操作系統(tǒng):Win8.1
② IDE:VS2015 Pro
③ 單元測(cè)試框架:Nunit致开,版本號(hào):3.0.5813.39033
④ 單元測(cè)試輔助插件:NUnit3 Test Adapter(這個(gè)適用于VS2013/VS2015峰锁,Nunit Test Adpater 不支持VS2013/VS2015,這個(gè)Pi君親測(cè))用這個(gè)插件可以將Nunit單元測(cè)試的結(jié)果放到VS自帶的測(cè)試資源管理器中顯示双戳,比較方便虹蒋。
⑤ 還有第二個(gè)更好的選擇,VS2015的CodeRush插件飒货,這簡(jiǎn)直就是個(gè)測(cè)試神器魄衅,強(qiáng)烈推薦,沒有之一塘辅,后續(xù)出文單說此神器晃虫。
⑥ Moq框架,在需要Mock時(shí)使用扣墩,VS2015的配置可以通過NuGet程序包管理器配置哲银,細(xì)節(jié)不贅述,可留言分享沮榜;
⑦ Ninject框架盘榨,依賴注入框架喻粹;
⑧ Fluent NHibernate ORM框架;
⑨ .net4.0
項(xiàng)目組織結(jié)構(gòu)
個(gè)人習(xí)慣蟆融,在開始一個(gè)新的項(xiàng)目前,先規(guī)劃好項(xiàng)目代碼及相關(guān)文件的路徑守呜,因?yàn)檫@個(gè)項(xiàng)目是Pi君github上的項(xiàng)目(非公司代碼)型酥,所以在github文件夾下,新建一個(gè)子文件夾查乒,并以該項(xiàng)目命名FBGame弥喉,繼續(xù)在該文件夾下新建兩個(gè)子文件夾libs和src,其中l(wèi)ibs存放第三方庫(kù)或框架玛迄,src存放項(xiàng)目代碼由境。
把需要依賴的框架或類庫(kù)復(fù)制到libs中,并在src文件夾下新建一個(gè)空的解決方案,命名為FBGame虏杰,然后新建一些項(xiàng)目:
→ 建一個(gè)類庫(kù)項(xiàng)目讥蟆,命名為FBGame.Core;
→ 建一個(gè)類庫(kù)項(xiàng)目纺阔,作為該解決方案的單元測(cè)試項(xiàng)目瘸彤,命名為FBGame.UnitTests;
→ 建一個(gè)類庫(kù)項(xiàng)目笛钝,作為該解決方案的集成測(cè)試項(xiàng)目质况,命名為FBGame.IntegrationTests
→ 建一個(gè)WinForm項(xiàng)目,作為該解決方案的客戶端玻靡,命名為FBGame.WinClient结榄。
OK,這個(gè)項(xiàng)目的基本組織結(jié)構(gòu)基本成型囤捻。
在后續(xù)的開發(fā)過程中潭陪,這個(gè)結(jié)構(gòu)可能會(huì)修改,但是從一開始就保持結(jié)構(gòu)的簡(jiǎn)單和清晰是個(gè)好習(xí)慣最蕾,倘若在開發(fā)和迭代過程中依溯,想添加一個(gè)新的項(xiàng)目,那首先要反問自己“即將要添加的部分必須屬于一個(gè)獨(dú)立的項(xiàng)目嗎瘟则?”黎炉,如果不一定,那就暫時(shí)不要新建醋拧,倘若必須新建慷嗜,也要考慮和現(xiàn)有項(xiàng)目之間的關(guān)系,最好在新項(xiàng)目命名的時(shí)候就充分體現(xiàn)這種關(guān)系丹壕。
需求回顧
首先庆械,回顧一下前文中這個(gè)項(xiàng)目的需求描述:
① 一個(gè)足球比賽類小游戲,用戶可以通過鍵盤操控控球球員前進(jìn)/后退/左轉(zhuǎn)/右轉(zhuǎn)/加速/傳球/射門菌赖;如果用戶控制的球隊(duì)沒有球權(quán)缭乘,則用戶可以切換控制球員進(jìn)行鏟球/防守;用戶可以控制游戲開始琉用,設(shè)置游戲時(shí)間堕绩,一般有兩支球隊(duì)進(jìn)行比賽;
② 每個(gè)球隊(duì)有一個(gè)教練邑时,有十一個(gè)球員奴紧,有自己的球隊(duì)隊(duì)形,用戶可以自己調(diào)整針對(duì)特定隊(duì)形的球員站位晶丘,有自己的隊(duì)服/隊(duì)徽黍氮;
③ 有一個(gè)管理員賬號(hào),管理員可以管理球隊(duì)相關(guān)數(shù)據(jù),包括球員數(shù)據(jù)/教練數(shù)據(jù)/隊(duì)形數(shù)據(jù)/隊(duì)服數(shù)據(jù)/隊(duì)徽數(shù)據(jù)沫浆。
OK觉壶,一般到此可以根據(jù)需求進(jìn)行需求分析,確定需求優(yōu)先級(jí)......這是一般的開發(fā)過程件缸,如果考慮TDD铜靶,也就是先添加針對(duì)功能的測(cè)試用例而已~但是,TDD真的就是這樣的他炊?争剿!PI君不曉得,一起探索下吧~
0次迭代
本次迭代的用戶場(chǎng)景
既然痊末,我們是要做一個(gè)游戲蚕苇,游戲前端界面在前文中已有描述,可以簡(jiǎn)單的理解為C/S結(jié)構(gòu)凿叠,真正需要考慮的是給前端提供功能涩笤,這樣,核心的功能(陰影部分描述)可以通過一些場(chǎng)景來描述:
① 當(dāng)比賽時(shí)間完成時(shí)盒件,比賽結(jié)束蹬碧;
② 當(dāng)足球進(jìn)入A隊(duì)球門時(shí),B隊(duì)得分炒刁,球權(quán)交給B隊(duì)恩沽,在球場(chǎng)中心位置發(fā)球繼續(xù)開始比賽;
③ 當(dāng)足球被A隊(duì)隊(duì)員踢出界時(shí)翔始,球權(quán)交給B隊(duì)罗心,B隊(duì)隊(duì)員在出界位置發(fā)球繼續(xù)比賽;
④ 球員根據(jù)唯一動(dòng)作標(biāo)識(shí)進(jìn)行位置更新(前進(jìn)/后退/左轉(zhuǎn)/右轉(zhuǎn)/加速)城瞎,球員有指定的速度(前進(jìn)速度/后退速度/轉(zhuǎn)向速度)和當(dāng)前方向渤闷,前進(jìn)/后退/加速時(shí),更新球員的位置脖镀,左轉(zhuǎn)/右轉(zhuǎn)更新球員的前進(jìn)方向飒箭;
⑤ 足球根據(jù)受控狀態(tài),加速度认然,速度补憾,位置漫萄,方向卷员,時(shí)刻更新足球位置;
⑥ 球權(quán)更新腾务,根據(jù)足球的空間位置和所有球員的運(yùn)動(dòng)狀態(tài)<空間位置>毕骡,計(jì)算所有球員(設(shè)置占有球權(quán)的邏輯)占有足球的概率,并返回概率最大者為控球球員。
當(dāng)然未巫,這些并不包含全部的需求(甚至不到完整需求的20%)窿撬,但是,這是一個(gè)Core叙凡,在實(shí)現(xiàn)這些功能以后劈伴,足球游戲是可玩的,好不好玩先不用管~核心需求關(guān)系這個(gè)項(xiàng)目的核心價(jià)值握爷,只有保證Core實(shí)現(xiàn)了跛璧,這個(gè)項(xiàng)目才有延續(xù)推進(jìn)的可能,對(duì)于用戶而言新啼,也存在一個(gè)迭代需求的依據(jù)追城,否則,用戶總是看不到項(xiàng)目進(jìn)展燥撞,總是不能上手使用座柱,開發(fā)和憋大招一樣,大家都互相憋著物舒,憋著憋著就沒了耐性色洞,項(xiàng)目也多半會(huì)流產(chǎn)~
網(wǎng)上一些大牛們?cè)诿枋鲇脩魣?chǎng)景上有一些很好的建議,Pi君引用過來冠胯,一起學(xué)習(xí):
“將較大的用戶情景分解為較小的功能锋玲,這些功能應(yīng)當(dāng)短小,簡(jiǎn)單涵叮,獨(dú)立惭蹂,可測(cè)。”——James Bender&&Jeff McWherter
用戶場(chǎng)景的功能列表
1 當(dāng)比賽時(shí)間完成時(shí)割粮,比賽結(jié)束盾碗;
1.1 一個(gè)獨(dú)立線程計(jì)時(shí)器類,可以設(shè)置時(shí)間舀瓢,開始計(jì)時(shí)廷雅,當(dāng)計(jì)時(shí)結(jié)束時(shí)返回結(jié)束狀態(tài);
2 當(dāng)足球進(jìn)入A隊(duì)球門時(shí)京髓,B隊(duì)得分航缀,球權(quán)交給B隊(duì),在球場(chǎng)中心位置發(fā)球繼續(xù)開始比賽堰怨;
2.1 一個(gè)球門類芥玉,用來描述球門,傳入足球的三維坐標(biāo)可以判斷是否進(jìn)球备图;
2.2 一個(gè)游戲信息服務(wù)類灿巧,這是一個(gè)獨(dú)立線程赶袄,監(jiān)控進(jìn)球發(fā)生事件,更新計(jì)算比分抠藕,更新球權(quán)狀態(tài)饿肺,更新足球位置;
2.3 一個(gè)全局變量盾似,球場(chǎng)中心位置敬辣;
3 當(dāng)足球被A隊(duì)隊(duì)員踢出界時(shí),球權(quán)交給B隊(duì)零院,B隊(duì)隊(duì)員在出界位置發(fā)球繼續(xù)比賽购岗;
3.1 一個(gè)球場(chǎng)邊界類,根據(jù)足球的位置判斷界內(nèi)界外门粪,當(dāng)足球出界時(shí)喊积,返回當(dāng)前足球位置;
3.2 參看2.2
4 球員根據(jù)唯一動(dòng)作標(biāo)識(shí)進(jìn)行位置更新(前進(jìn)/后退/左轉(zhuǎn)/右轉(zhuǎn)/加速)玄妈,球員有指定的速度(前進(jìn)速度/后退速度/轉(zhuǎn)向速度)和當(dāng)前方向乾吻,前進(jìn)/后退/加速時(shí),更新球員的位置拟蜻,左轉(zhuǎn)/右轉(zhuǎn)更新球員的前進(jìn)方向绎签;
4.1 一個(gè)球員類,根據(jù)動(dòng)作指示符酝锅,進(jìn)行位置及方向的更新诡必;
5 足球根據(jù)受控狀態(tài),加速度搔扁,速度爸舒,位置,方向稿蹲,時(shí)刻更新足球位置扭勉;
5.1 一個(gè)足球類,包含足球的運(yùn)動(dòng)屬性苛聘,方向和三維位置信息涂炎,這是一個(gè)單例類;
5.2 一個(gè)足球服務(wù)類设哗,這是一個(gè)獨(dú)立線程唱捣,在比賽時(shí)間內(nèi),每間隔單位時(shí)間根據(jù)當(dāng)前時(shí)刻下的足球?qū)ο笮畔⒏伦闱虻娜S位置信息网梢;
6 球權(quán)更新震缭,根據(jù)足球的空間位置和所有球員的運(yùn)動(dòng)狀態(tài)<空間位置>,計(jì)算所有球員(設(shè)置占有球權(quán)的邏輯)占有足球的概率澎粟,并返回概率最大者為控球球員蛀序。
6.1 游戲信息服務(wù)類欢瞪,每間隔單位時(shí)間活烙,計(jì)算所有球員(設(shè)置占有球權(quán)的邏輯)占有足球的概率徐裸,并更新概率最大者為控球球員;
實(shí)現(xiàn)場(chǎng)景1——>當(dāng)比賽時(shí)間完成時(shí)啸盏,比賽結(jié)束
1.1 一個(gè)獨(dú)立線程計(jì)時(shí)器類重贺,可以設(shè)置時(shí)間,開始計(jì)時(shí)后每隔單位時(shí)間返回計(jì)時(shí)結(jié)果(字符串)回懦,當(dāng)計(jì)時(shí)結(jié)束時(shí)返回結(jié)束狀態(tài)气笙;
定義一個(gè)計(jì)時(shí)器類,給計(jì)時(shí)器設(shè)置時(shí)間t怯晕,當(dāng)計(jì)時(shí)器開始計(jì)時(shí)時(shí)潜圃,根據(jù)不同的時(shí)刻(不到t,超過t舟茶,等于t)谭期,獲取計(jì)時(shí)字符串和狀態(tài),即:
TestCase-1:不到t吧凉,返回時(shí)刻字符串和Running狀態(tài)隧出;
TestCase-2:超過或到達(dá)t,返回時(shí)刻字符串和Over狀態(tài)狀態(tài)阀捅。
注:其中Running用整數(shù)1標(biāo)識(shí)胀瞪,Over用0標(biāo)識(shí)。
在單元測(cè)試的構(gòu)建中饲鄙,采用BDD(行為驅(qū)動(dòng)開發(fā))風(fēng)格的命名規(guī)則是個(gè)好的建議凄诞,在單元測(cè)試的類名及方法名的定義中將用戶場(chǎng)景進(jìn)行描述,很直觀忍级,易于和業(yè)務(wù)人員溝通幔摸。簡(jiǎn)單說來就是三個(gè)步驟:
1. when->一般是某類場(chǎng)景單元測(cè)試的基類,保存但元測(cè)試的運(yùn)行環(huán)境颤练,用在此處可以這寫:
public class when_working_with_the_TimeCounter_start{}既忆;
2. and->一般是具體某種情形下的場(chǎng)景的單元測(cè)試類,繼承when定義的基類嗦玖,共享測(cè)試環(huán)境,例如:
public class and_by_the_time_point : when_working_with_the_TimeCounter_start{}宇挫;
3. which...then->一般是測(cè)試用例器瘪,即測(cè)試方法的定義翠储,例如:
public void which_in_the_time_then_TimeCounter_status_should_be_running()绘雁;
在使用BDD的命名規(guī)則時(shí)庐舟,可以直接引用BDD的命名庫(kù),但是Pi君暫時(shí)還沒有感受到使用nBehave類似的BDD語法糖帶來的優(yōu)勢(shì)住拭,所以先借用一下命名規(guī)則而已挪略,(對(duì)于BDD而言杠娱,這連皮毛都不算~)測(cè)試框架谱煤,直接使用NUnit刘离。
TestCase-1:
OK,構(gòu)建單元測(cè)試如下:
Pi君省去了一步一步的添加過程,當(dāng)前這個(gè)狀態(tài)可以編譯通過疲憋,但是運(yùn)行時(shí)會(huì)報(bào)錯(cuò)~
因?yàn)開timeCounter沒有實(shí)例缚柳,只有一個(gè)接口的定義而已:
接下來需要添加一個(gè)實(shí)現(xiàn)ITimeCounter的實(shí)體類:
然后在when_working_with_the_TimeCounter_start添加對(duì)_timeCounter的定義:
運(yùn)行測(cè)試彩掐,失敗了堵幽,原因是:
計(jì)算當(dāng)前時(shí)刻字符串的方法不能通過測(cè)試弹澎,調(diào)整該方法的代碼:
在TDD過程中,小步前進(jìn)總是好的殴胧,和獲取計(jì)時(shí)器的狀態(tài)一樣团滥,也可以直接在獲取當(dāng)前時(shí)刻字符串的方法上返回測(cè)試用例中需要的時(shí)刻,但是timePoint=_radom.Next(0,10)拱燃,這是個(gè)固定范圍內(nèi)的隨機(jī)數(shù)扼雏,次奧夯膀,寫測(cè)試案例的小朋友真是機(jī)靈......這意味著如果想通過測(cè)試诱建,不得不“前進(jìn)”一大步了俺猿。
為了讓計(jì)時(shí)器可以返回計(jì)時(shí)時(shí)間段內(nèi)的不同時(shí)刻格仲,給計(jì)時(shí)器添加幾個(gè)成員:
分別記錄時(shí)刻及其對(duì)應(yīng)的字符串谊惭,二者有個(gè)一一對(duì)應(yīng)的轉(zhuǎn)換關(guān)系侮东,用一個(gè)功能函數(shù)來實(shí)現(xiàn)這種轉(zhuǎn)換:
哎呀媽呀悄雅,步子太大宽闲,容易扯著蛋......
謹(jǐn)慎起見容诬,再跑一次原來的測(cè)試,次奧姚建,又失敗了掸冤,看來步子還是太小啊~~繼續(xù)扯:
因?yàn)闇y(cè)試用例已經(jīng)說明計(jì)時(shí)器是個(gè)獨(dú)立線程,所以給start函數(shù)添加這段代碼:
再跑一次測(cè)試,次奧包斑,又是紅叉叉~這次問題出在哪里涕俗?多線程~
_currentPoint和_currentPointStr其實(shí)是共享資源再姑,使用時(shí)元镀,要加鎖的~這個(gè)不能忘~添加幾行代碼:
再跑一次測(cè)試栖疑,還是失敗遇革,依然是多線程的問題澳淑,這一次是多線程同步的問題,分析如下:
主線程的計(jì)時(shí)時(shí)刻和計(jì)時(shí)器線程的計(jì)時(shí)時(shí)刻不同步,導(dǎo)致測(cè)試失敗氢拥,注意嫩海,這不是共享資源的同步問題叁怪,是時(shí)刻同步問題,So涣觉,使用同步事件和等待句柄(AutoResetEvent或ManualResetEvent)官册,不熟悉的看官直接MSDN搜查下:計(jì)時(shí)器是一個(gè)獨(dú)立線程膝宁,所以使用AutoResetEvent比較簡(jiǎn)單,修改代碼如下:
在TimeCounter中添加成員合蔽,并初始化:
然后在Run()方法中的while循環(huán)下辈末,添加WaitOne句柄:
然后再ITimeCounter接口中增加對(duì)AutoResetEvent事件的控制方法及實(shí)現(xiàn):
OK轰枝,這樣鞍陨,客戶端線程(其實(shí)就是這里的測(cè)試線程)就可以控制計(jì)時(shí)器的計(jì)時(shí)過程啦,讓你三更掛掉缭裆,絕對(duì)不會(huì)等到四更~接下來修改測(cè)試代碼:
在主線程“計(jì)時(shí)過程”中控制計(jì)時(shí)器的計(jì)時(shí)過程澈驼,以此來保證同步(其實(shí)只是相對(duì)同步缝其,基于線程的概念内边,如果沒有硬件的外在支持待锈,在時(shí)間上的絕對(duì)同步是不現(xiàn)實(shí)的),運(yùn)行測(cè)試阳惹,OK眶俩,通過~
注:https://msdn.microsoft.com/zh-cn/library/ms173179(VS.80).aspx——線程同步MSDN鏈接
總是編寫最小的代碼量來讓測(cè)試通過和小步前進(jìn)不沖突颠印,后者更偏向于說明運(yùn)行單元測(cè)試的時(shí)機(jī)线罕。
TestCase-2:
構(gòu)建單元測(cè)試用例:超過或到達(dá)t钞楼,返回時(shí)刻字符串和Over狀態(tài);
阿西吧,這個(gè)測(cè)試沒通過~Why宛琅?問題出在最后一個(gè)斷言嘿辟,計(jì)時(shí)器的最大時(shí)刻就是t红伦,對(duì)于超出t的時(shí)刻昙读,計(jì)時(shí)器是無法獲取的,修改下某残,斷言計(jì)時(shí)器當(dāng)前時(shí)刻是其最大時(shí)刻:
運(yùn)行測(cè)試玻墅,OK澳厢,通過~ ?
針對(duì)第一個(gè)場(chǎng)景是不是就結(jié)束了剩拢,是嗎徐伐?對(duì)于TDD而言,這只是一個(gè)開始啊親~~
重構(gòu)——每次單元測(cè)試運(yùn)行通過以后角雷,都應(yīng)該考慮重構(gòu)
重構(gòu)勺三,是一個(gè)過程工具吗坚,每次測(cè)試通過之后呆万,都要重構(gòu)桑嘶,不僅是業(yè)務(wù)邏輯代碼逃顶,也包括測(cè)試代碼充甚。
先檢查業(yè)務(wù)邏輯代碼:TimeCounter伴找,還好技矮,沒有重復(fù)衰倦,命名基本達(dá)意,先過我磁,再看看單元測(cè)試代碼:
藍(lán)色框選中的代碼模擬的是主線程的計(jì)時(shí)芋哭,同時(shí)關(guān)聯(lián)控制計(jì)時(shí)器的計(jì)時(shí)郁副,兩個(gè)測(cè)試用例都需要這個(gè)功能存谎,所以可以提取方法:
兩個(gè)測(cè)試案例代碼修改為:
因?yàn)镸ockTimeCount方法草雕,只和when_working_with_the_TimeCounter_start類有關(guān)系固以,所以把該方法上提至基類憨琳,更改訪問權(quán)限為protected篙螟。
切記切記遍略,你的每一次修改,都要運(yùn)行所有的但元測(cè)試下愈,至少對(duì)于目前的狀態(tài)是這樣的(不然势似,為啥要寫測(cè)試嘞~~~)履因。
OK栅迄,重構(gòu)暫時(shí)先到這里霞篡。
對(duì)于用戶場(chǎng)景1:
1 當(dāng)比賽時(shí)間完成時(shí),比賽結(jié)束污淋;
1.1 一個(gè)獨(dú)立線程計(jì)時(shí)器類寸爆,可以設(shè)置時(shí)間赁豆,開始計(jì)時(shí)魔种,當(dāng)計(jì)時(shí)結(jié)束時(shí)返回結(jié)束狀態(tài)节预;
TDD開發(fā)過程告一段落属韧,稍后宵喂,Pi君以另一篇文字記述用戶場(chǎng)景2的TDD過程锅棕。
有關(guān)FBGame項(xiàng)目源碼的github地址:
https://github.com/fei090620/FBGame.git
源碼會(huì)隨著文字的更新而更新哲戚。