iOS開(kāi)發(fā)進(jìn)階之單元測(cè)試
開(kāi)始之前
本文側(cè)重講述如何在iOS程序的開(kāi)發(fā)過(guò)程中使用單元測(cè)試。使用Xcode自帶的OCUnit作為測(cè)試框架。
一、單元測(cè)試概述
單元測(cè)試作為敏捷開(kāi)發(fā)實(shí)踐的組成之一,其目的是提高軟件開(kāi)發(fā)的效率,維持代碼的健康性。其目標(biāo)是證明軟件能夠正常運(yùn)行,而不是發(fā)現(xiàn)bug(發(fā)現(xiàn)bug這一目的與開(kāi)發(fā)成本是正相關(guān)的,雖然發(fā)現(xiàn)bug是保證軟件質(zhì)量的一種手段,但是很顯然這與降低軟件開(kāi)發(fā)成本這一目的背道而馳)。它是對(duì)軟件質(zhì)量的一種保證,例如重構(gòu)之后我們需要保證軟件產(chǎn)品的正常運(yùn)行。
很多人認(rèn)為編寫單元測(cè)試沒(méi)有用是認(rèn)為單元測(cè)試并不能保證一定能減少bug發(fā)生的幾率,而由于編寫單元測(cè)試一定會(huì)花費(fèi)一定的時(shí)間與精力,因而必然的會(huì)增加成本。客觀的說(shuō),造成這種原因很大的程度上是程序員的水平不夠高。我認(rèn)為使用使用單元測(cè)試帶來(lái)巨大好處的必要條件如下所示:
● 程序員本身的編程水平--是否有較多的代碼經(jīng)驗(yàn),是否熟練掌握重構(gòu)
● 程序員對(duì)項(xiàng)目的認(rèn)知--是否能正確理解軟件或模塊的需求
● 項(xiàng)目質(zhì)量--是否穩(wěn)定,是否長(zhǎng)期多版本,是否需要應(yīng)對(duì)較多變化
如果程序員水平較高,對(duì)需求理解較為清晰,項(xiàng)目需要面對(duì)較多的變化,那么毫無(wú)疑問(wèn)單元測(cè)試對(duì)于軟件非常有益。假如軟件功能簡(jiǎn)單且開(kāi)發(fā)周期短,不需要進(jìn)行復(fù)雜的維護(hù)工作,那么單元測(cè)試的意義并不大。
優(yōu)秀的單元測(cè)試實(shí)踐的好處:
● 好的單元測(cè)試就是一份好的文檔,并且比文檔更能為程序員所接受,它直接描述了測(cè)試員對(duì)受測(cè)代碼的結(jié)果所持的預(yù)期。
● 當(dāng)代碼由別人維護(hù)時(shí)(或自己進(jìn)行重構(gòu)時(shí)),通過(guò)單元測(cè)試的約束,才能保證在加入新功能或修改舊功能時(shí)代碼的正確性。
● 由于單元測(cè)試的自動(dòng)化執(zhí)行,保證了在整個(gè)開(kāi)發(fā)流程中代碼都會(huì)被測(cè)試,這非常符合XP思想。
● 保證在面對(duì)軟件功能的變化時(shí),程序員可以較為放心的進(jìn)行代碼重構(gòu),而不必?fù)?dān)心是否破壞了原有功能。
● 好的單元測(cè)試可以降低bug數(shù)量,而對(duì)于項(xiàng)目管理來(lái)說(shuō),修改bug這個(gè)過(guò)程是無(wú)法制定計(jì)劃的,可以使軟件的開(kāi)發(fā)流程更容易掌控。
● 可以由老程序員編寫描述某個(gè)類行為的測(cè)試,以此指導(dǎo)新程序員對(duì)類的編碼。
● ……
好處還有很多,但最重要的一點(diǎn)就是保證了軟件質(zhì)量的同時(shí),由于減少bug和應(yīng)對(duì)變化造成的回歸bug的產(chǎn)生等,提高了勞動(dòng)生產(chǎn)率。而且,在敏捷流程中,使用單元測(cè)試是必須掌握的手段,否則就沒(méi)發(fā)保證重構(gòu)的正確性,從而造成代碼無(wú)法面對(duì)變化。
二、iOS的單元測(cè)試概述
剛接觸客戶端編程時(shí),我在很長(zhǎng)一段時(shí)間內(nèi)都想不通對(duì)于客戶端程序如何編寫單元測(cè)試。單元測(cè)試本質(zhì)上說(shuō)白了就是用一些斷言來(lái)判定結(jié)果,而這種方式是如何應(yīng)用到具有復(fù)雜交互的界面測(cè)試上來(lái)的呢?
我們要做的就是將客戶端代碼轉(zhuǎn)化為易于測(cè)試的代碼。什么樣的代碼易于測(cè)試呢?它至少是這樣的:
1、被測(cè)方法需要產(chǎn)生可測(cè)量的結(jié)果。
2、類之間的關(guān)系應(yīng)該是松耦合的。
其中第一條是必要條件。使用斷言這種形式指明了測(cè)試的方法最終要造成某些可以度量的結(jié)果。因而,我們需要盡量的將展示和業(yè)務(wù)邏輯分離開(kāi)來(lái)。展示的代碼是沒(méi)法測(cè)試的,例如有的方法只是播放動(dòng)畫。而業(yè)務(wù)邏輯最終都會(huì)造成一些數(shù)據(jù)的改變,這是容易測(cè)試的。
大略的講,作為一個(gè)iOS程序員來(lái)說(shuō),首先要了解一個(gè)叫做MVC的模式。這個(gè)模式定義了Cocoa Touch框架的總體結(jié)構(gòu)。在iOS程序中,我們也需要按照這種模式進(jìn)行界面代碼的編寫。這樣設(shè)計(jì)出來(lái)的類具有較好的結(jié)構(gòu),且比較適合于做單元測(cè)試。
然后一定要懂得不停重構(gòu)代碼,這樣我們才能使代碼不停地改善,不停地變得更加適合單元測(cè)試。
有一些框架可以幫助大家更好的測(cè)試,分別是OCUnit、GTM、GHUnit、CATCH、OCMock,但目前對(duì)我來(lái)說(shuō),OCUnit足夠用了。作為蘋果官方提供的測(cè)試框架,它最大的優(yōu)點(diǎn)就是簡(jiǎn)單易用。
三、單元測(cè)試實(shí)踐
下面是一些我所理解的單元測(cè)試中比較好的實(shí)踐。
顧名思義,單元測(cè)試面向的對(duì)象是單元,這個(gè)專有名詞源自編譯器領(lǐng)域的術(shù)語(yǔ)“編譯單元”。在面向過(guò)程中,指的是函數(shù),而在面向?qū)ο笾?,指的通常就?#8220;類”。因而,每個(gè)功能類都應(yīng)該提供對(duì)應(yīng)的單元測(cè)試。
實(shí)踐1:每個(gè)功能類都應(yīng)提供單元測(cè)試,且每一個(gè)測(cè)試類,只依賴于其要測(cè)試的受測(cè)類。使用偽造對(duì)象可以避免對(duì)其他類的依賴。
解釋:保證一個(gè)測(cè)試類只關(guān)注一個(gè)被測(cè)類,當(dāng)測(cè)試不通過(guò)時(shí),就能迅速的定位到是誰(shuí)發(fā)生了錯(cuò)誤,而不會(huì)受到其他類的干擾。
簡(jiǎn)單的數(shù)據(jù)類等可以不提供,但是要保證該測(cè)試的都要覆蓋到。并不存在一種合適的度量指標(biāo)可以量化地判斷某種單元測(cè)試方案是否成功。常用的標(biāo)準(zhǔn)(代碼覆蓋率和成功執(zhí)行的測(cè)試用例數(shù))都可以在受測(cè)軟件的質(zhì)量不變的情況下人為的修改(作假)。當(dāng)然,在無(wú)法確保程序員素質(zhì)的情況下,作為沒(méi)有辦法的辦法,使用這種標(biāo)準(zhǔn)也是可以的(或者無(wú)奈的說(shuō),必須的)。單元測(cè)試需要程序員自己把關(guān),關(guān)注哪些功能確實(shí)需要測(cè)試覆蓋。這也就是前面所說(shuō)的一些程序員不相信單元測(cè)試可以提高生產(chǎn)率的理由--它更多的依賴于程序員的素質(zhì),這是沒(méi)有保證的。但同樣的,由于敏捷是一種以人為本的思想實(shí)踐,因而這種行為似乎又是一種必然。
實(shí)踐1.1:使用偽造類避免對(duì)其他類的依賴。
解釋:避免依賴的一種手段。
例如,某個(gè)被測(cè)的方法聲明是這樣的:
-(void)xxxx:(Person *)person;
如果測(cè)試時(shí)傳入Person的話,就造成了測(cè)試類依賴于兩個(gè)類。當(dāng)由于person中的錯(cuò)誤引發(fā)測(cè)試不通過(guò)時(shí),就不能迅速的定位到受測(cè)類中是否有問(wèn)題。遇到這種情況,就可以使用偽造類。假如方法中只使用了person的一個(gè)屬性name,那么可以將方法名重構(gòu)為
-(void)xxxx:(id)person;(此處id有待商榷,只是這樣做最簡(jiǎn)單)
然后在單元測(cè)試的target中添加只包含name屬性的fakePerson來(lái)作為偽造類。這樣,一旦發(fā)生錯(cuò)誤就可以迅速的推測(cè)出錯(cuò)誤的來(lái)源。
實(shí)踐1.2:使用偽造環(huán)境避免其他環(huán)境的干擾。
解釋:適合于異步的方法測(cè)試。
很經(jīng)常遇到的一種情況是測(cè)試有網(wǎng)絡(luò)環(huán)境的代碼。由于異步的存在,這會(huì)造成測(cè)試代碼不好寫。一種簡(jiǎn)單的解決方法是,我們假定網(wǎng)絡(luò)一定是通暢的,則我們測(cè)試的代碼將分為兩部分,即拼裝發(fā)送功能和接收解析功能。假如發(fā)送和接收功能各自都能通過(guò)測(cè)試,那么我們大約可以確定這個(gè)異步方法的正確性。另一種方法是使用GHUnit,它支持異步代碼的測(cè)試。
實(shí)踐2:測(cè)試用例(方法)名應(yīng)該是自解釋的且是獨(dú)立的。
解釋:基本功。
如果被測(cè)試類的名稱是XXX,那么測(cè)試類可以命名為XXXTests。而對(duì)于其中要測(cè)試的功能,命名應(yīng)該是自解釋的。這可以在發(fā)現(xiàn)錯(cuò)誤時(shí)盡快的定位問(wèn)題所在。例如,如果某個(gè)屬性obj應(yīng)該是非空的,那么我們可以將其命名為:
-(void)testObjNotNil{}
每個(gè)方法目標(biāo)應(yīng)該是單一的,大多數(shù)情況下每個(gè)方法內(nèi)都只有一個(gè)斷言語(yǔ)句;方法不應(yīng)該依賴于其他方法的結(jié)果作為輸入,保證原子性。
實(shí)踐3:斷言語(yǔ)句需要解釋測(cè)試者的意圖。
解釋:基本功
每種單元測(cè)試框架都提供了很多斷言語(yǔ)句,從根本上來(lái)說(shuō)它們都是一樣的。但是測(cè)試者需要根據(jù)自己的目的選擇適當(dāng)?shù)恼Z(yǔ)句,這樣才可以讓別人閱讀測(cè)試代碼時(shí)理解用例設(shè)計(jì)的目的。例如對(duì)于STAssertNil和STAssertNotNil等等。
實(shí)踐4:判斷某個(gè)意圖有沒(méi)有達(dá)到的很好的方法是檢測(cè)方法影響的數(shù)據(jù)有沒(méi)有合理的變化。
解釋:基本功
由于單元測(cè)試是使用斷言語(yǔ)句來(lái)做判斷的,因而最容易做的就是判斷數(shù)據(jù)的變化。這也就限定了單元測(cè)試能測(cè)試的方法范圍,即引起數(shù)據(jù)變化的方法。對(duì)于一些純展示的方法,例如播放一段特效,這種方法是無(wú)法靠單元測(cè)試來(lái)進(jìn)行約束的。測(cè)試數(shù)據(jù)的特性包括取值范圍(int、float等),排列順序(NSArray等),類型等等。
實(shí)踐5:運(yùn)用重構(gòu)的手段使方法變得易于被測(cè)試。
解釋:?jiǎn)卧獪y(cè)試是保障重構(gòu)安全的手段,重構(gòu)也可以使代碼易于被測(cè)試。
什么樣的代碼是容易進(jìn)行單元測(cè)試的?最簡(jiǎn)單的一點(diǎn)就是,每個(gè)被測(cè)方法都應(yīng)該是功能單一的。當(dāng)然,這也是代碼規(guī)范中應(yīng)該做到的。方法的功能單一,則測(cè)試方法的斷言也會(huì)比較好確定。如果你發(fā)現(xiàn)某個(gè)方法很難進(jìn)行測(cè)試,則就應(yīng)該對(duì)這個(gè)方法進(jìn)行拆分重構(gòu)。
實(shí)踐5.1:面向抽象設(shè)計(jì)類之間的關(guān)系。
解釋:利于偽造類的實(shí)現(xiàn)。
類之間通訊如果依賴于抽象(接口),則可以較容易的使用偽造類。參照實(shí)踐1.1。
實(shí)踐6:運(yùn)用自上而下的方式構(gòu)建類。
解釋:自上而下的方式可以使類的功能明確,類的構(gòu)成將會(huì)清晰緊湊,不會(huì)出現(xiàn)一些廢方法。
先確定類需要負(fù)擔(dān)的責(zé)任,以此來(lái)確定類具有的公有方法以及屬性。通過(guò)重構(gòu)將公有方法中的代碼轉(zhuǎn)化為私有方法,以使方法盡量短小緊湊。
實(shí)踐6.1:應(yīng)對(duì)所有暴露的屬性和方法提供測(cè)試,私有方法則不必。
解釋:如果運(yùn)用自上而下的方式構(gòu)建類,則理論上私有方法應(yīng)該都是公有方法重構(gòu)而得到的。實(shí)際上測(cè)試公有方法時(shí)這些私有方法都應(yīng)該被測(cè)試到了。而且,由于私有方法相對(duì)公有方法來(lái)說(shuō)發(fā)生變動(dòng)的可能性很大,會(huì)造成不必要的修改測(cè)試代碼的成本。
回調(diào)方法不屬于私有方法,也需要進(jìn)行測(cè)試。
實(shí)踐6.2:回調(diào)方法的測(cè)試方法是直接調(diào)用。
解釋:基本功
由于回調(diào)方法一般是異步和不可觸發(fā)的(按正常流程),例如網(wǎng)絡(luò)事件的返回和按下按鈕的觸發(fā)事件。因而,測(cè)試的時(shí)候要直接調(diào)用來(lái)對(duì)其流程進(jìn)行檢測(cè)。例如某個(gè)按鈕的touch up inside事件:
-(void)buttonPressed:(id)sender;
可以根據(jù)方法中用到的方法、屬性偽造一個(gè)FakeButton按鈕作為參數(shù)傳遞進(jìn)行測(cè)試。
實(shí)踐6.2:測(cè)試私有的方式,KVC、子類化和類別。
解釋:基本功。
遇到需要通過(guò)驗(yàn)證私有數(shù)據(jù)才能編寫的測(cè)試時(shí),可以考慮使用KVC和子類化。子類繼承于被測(cè)類,只包含于單元測(cè)試target,其作用就是在不該變受測(cè)類的情況下,使受測(cè)類具有某些易于被測(cè)的能力。
實(shí)踐7:變化需要新測(cè)試的支持。
解釋:保證測(cè)試的覆蓋度。
就像敏捷中提到的“改變需要抽象”一樣,在測(cè)試中改變需要新的測(cè)試。當(dāng)然,度依然由程序員自己掌控。
四、一般流程
使用OCUnit最大的好處就是流程非常的簡(jiǎn)單,簡(jiǎn)單到讓你覺(jué)得非常愉悅。由于有XCode的支持,添加測(cè)試變得異常簡(jiǎn)單。只要在新建工程時(shí)勾選“Include Unit Tests”,就會(huì)自動(dòng)的加入一個(gè)示例。然后再需要添加新的單元測(cè)試時(shí),新建一個(gè)“Objective-C test case class”就可以了。
測(cè)試文件中,只要知道setUp是初始化的地方,tearDown是結(jié)束清理的地方,而且它們?cè)诿總€(gè)用例方法執(zhí)行時(shí)都會(huì)重新執(zhí)行--這保證了測(cè)試用例的原子性。然后知道每個(gè)測(cè)試用例都是以test作為前綴的,并且無(wú)返回值。然后在方法中編寫斷言語(yǔ)句就可以了。輸入STAssertxxxxx就可以看到它們的聯(lián)想提示。編寫完成后,執(zhí)行菜單Product->Test,單元測(cè)試就完成了!
五、測(cè)試驅(qū)動(dòng)(TDD)
敏捷當(dāng)中提到了TDD這種開(kāi)發(fā)方式。TDD的主旨是使開(kāi)發(fā)者對(duì)其編寫的代碼更有信心,使開(kāi)發(fā)者修改代碼時(shí)心里更加踏實(shí)。對(duì)于其總結(jié),還是引用原文比較妥當(dāng):“測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的妙處即在于,它以需求為引領(lǐng),通過(guò)測(cè)試的形式,來(lái)指導(dǎo)開(kāi)發(fā)者進(jìn)行軟件的設(shè)計(jì)與架構(gòu),并編寫出最為精煉的代碼,使得測(cè)試用例運(yùn)行通過(guò)。經(jīng)過(guò)適當(dāng)?shù)闹貥?gòu)之后,測(cè)試用例與產(chǎn)品代碼可達(dá)到較為健康的狀態(tài)。”也就是上面提到的,通過(guò)自上而下的形式設(shè)計(jì)類,通過(guò)單元測(cè)試來(lái)不停地審視和重構(gòu)類,從而達(dá)到代碼的健康。
如果在代碼寫完之后在編寫單元測(cè)試,那么就體現(xiàn)不出這種模式的好處了。這就好像寫完代碼再補(bǔ)文檔一樣,沒(méi)有什么意義。測(cè)試應(yīng)該在代碼開(kāi)始之前,或者在代碼編寫中不停地進(jìn)行編寫更新,這樣才能使代碼不停進(jìn)步。這也正是TDD的意思。
六、總結(jié)
單元測(cè)試的代碼如此簡(jiǎn)單,但是想寫好單元測(cè)試卻并不是一件簡(jiǎn)單的事情。它需要程序員比較深的功底。由于個(gè)人水平所限,有一些東西說(shuō)的比較啰嗦。把復(fù)雜問(wèn)題簡(jiǎn)單化是本事,任重而道遠(yuǎn)。希望大家可以在日常開(kāi)發(fā)中運(yùn)用好這種簡(jiǎn)潔高效的技術(shù)。
最后,作為一個(gè)中國(guó)人,祈福雅安。網(wǎng)上有一些人在這種事件上還在噴。我想說(shuō)的是,雖然這個(gè)世界是客觀存在的,但是每個(gè)人的世界觀確是主觀的。一個(gè)人觀察世界的角度決定了他認(rèn)為這個(gè)世界是什么樣的。中華民族的民族性格是內(nèi)斂的,在外在表現(xiàn)上似乎不如西方人張揚(yáng)。但是就像汶川一樣,全國(guó)人民在這種時(shí)刻表現(xiàn)出了我們這個(gè)民族內(nèi)在的優(yōu)秀品質(zhì)。老鼠屎在哪里都是老鼠屎,我以自己是中國(guó)人為榮。
posted on 2013-05-16 10:42 順其自然EVO 閱讀(164) 評(píng)論(0) 編輯 收藏