qileilove

          blog已經(jīng)轉(zhuǎn)移至github,大家請?jiān)L問 http://qaseven.github.io/

          iOS開發(fā)進(jìn)階之單元測試

           開始之前

            本文側(cè)重講述如何在iOS程序的開發(fā)過程中使用單元測試。使用Xcode自帶的OCUnit作為測試框架。

            一、單元測試概述

            單元測試作為敏捷開發(fā)實(shí)踐的組成之一,其目的是提高軟件開發(fā)的效率,維持代碼的健康性。其目標(biāo)是證明軟件能夠正常運(yùn)行,而不是發(fā)現(xiàn)bug(發(fā)現(xiàn)bug這一目的與開發(fā)成本是正相關(guān)的,雖然發(fā)現(xiàn)bug是保證軟件質(zhì)量的一種手段,但是很顯然這與降低軟件開發(fā)成本這一目的背道而馳)。它是對軟件質(zhì)量的一種保證,例如重構(gòu)之后我們需要保證軟件產(chǎn)品的正常運(yùn)行。

            很多人認(rèn)為編寫單元測試沒有用是認(rèn)為單元測試并不能保證一定能減少bug發(fā)生的幾率,而由于編寫單元測試一定會(huì)花費(fèi)一定的時(shí)間與精力,因而必然的會(huì)增加成本。客觀的說,造成這種原因很大的程度上是程序員的水平不夠高。我認(rèn)為使用使用單元測試帶來巨大好處的必要條件如下所示:

            ● 程序員本身的編程水平--是否有較多的代碼經(jīng)驗(yàn),是否熟練掌握重構(gòu)

            ● 程序員對項(xiàng)目的認(rèn)知--是否能正確理解軟件或模塊的需求

            ● 項(xiàng)目質(zhì)量--是否穩(wěn)定,是否長期多版本,是否需要應(yīng)對較多變化

            如果程序員水平較高,對需求理解較為清晰,項(xiàng)目需要面對較多的變化,那么毫無疑問單元測試對于軟件非常有益。假如軟件功能簡單且開發(fā)周期短,不需要進(jìn)行復(fù)雜的維護(hù)工作,那么單元測試的意義并不大。

            優(yōu)秀的單元測試實(shí)踐的好處:

            ● 好的單元測試就是一份好的文檔,并且比文檔更能為程序員所接受,它直接描述了測試員對受測代碼的結(jié)果所持的預(yù)期。

            ● 當(dāng)代碼由別人維護(hù)時(shí)(或自己進(jìn)行重構(gòu)時(shí)),通過單元測試的約束,才能保證在加入新功能或修改舊功能時(shí)代碼的正確性。

            ● 由于單元測試的自動(dòng)化執(zhí)行,保證了在整個(gè)開發(fā)流程中代碼都會(huì)被測試,這非常符合XP思想。

            ● 保證在面對軟件功能的變化時(shí),程序員可以較為放心的進(jìn)行代碼重構(gòu),而不必?fù)?dān)心是否破壞了原有功能。

            ● 好的單元測試可以降低bug數(shù)量,而對于項(xiàng)目管理來說,修改bug這個(gè)過程是無法制定計(jì)劃的,可以使軟件的開發(fā)流程更容易掌控。

            ● 可以由老程序員編寫描述某個(gè)類行為的測試,以此指導(dǎo)新程序員對類的編碼。

            ● ……

            好處還有很多,但最重要的一點(diǎn)就是保證了軟件質(zhì)量的同時(shí),由于減少bug和應(yīng)對變化造成的回歸bug的產(chǎn)生等,提高了勞動(dòng)生產(chǎn)率。而且,在敏捷流程中,使用單元測試是必須掌握的手段,否則就沒發(fā)保證重構(gòu)的正確性,從而造成代碼無法面對變化。

            二、iOS的單元測試概述

            剛接觸客戶端編程時(shí),我在很長一段時(shí)間內(nèi)都想不通對于客戶端程序如何編寫單元測試。單元測試本質(zhì)上說白了就是用一些斷言來判定結(jié)果,而這種方式是如何應(yīng)用到具有復(fù)雜交互的界面測試上來的呢?

            我們要做的就是將客戶端代碼轉(zhuǎn)化為易于測試的代碼。什么樣的代碼易于測試呢?它至少是這樣的:

            1、被測方法需要產(chǎn)生可測量的結(jié)果。

            2、類之間的關(guān)系應(yīng)該是松耦合的。

            其中第一條是必要條件。使用斷言這種形式指明了測試的方法最終要造成某些可以度量的結(jié)果。因而,我們需要盡量的將展示和業(yè)務(wù)邏輯分離開來。展示的代碼是沒法測試的,例如有的方法只是播放動(dòng)畫。而業(yè)務(wù)邏輯最終都會(huì)造成一些數(shù)據(jù)的改變,這是容易測試的。

            大略的講,作為一個(gè)iOS程序員來說,首先要了解一個(gè)叫做MVC的模式。這個(gè)模式定義了Cocoa Touch框架的總體結(jié)構(gòu)。在iOS程序中,我們也需要按照這種模式進(jìn)行界面代碼的編寫。這樣設(shè)計(jì)出來的類具有較好的結(jié)構(gòu),且比較適合于做單元測試。

            然后一定要懂得不停重構(gòu)代碼,這樣我們才能使代碼不停地改善,不停地變得更加適合單元測試。

            有一些框架可以幫助大家更好的測試,分別是OCUnit、GTM、GHUnit、CATCH、OCMock,但目前對我來說,OCUnit足夠用了。作為蘋果官方提供的測試框架,它最大的優(yōu)點(diǎn)就是簡單易用。

            三、單元測試實(shí)踐

            下面是一些我所理解的單元測試中比較好的實(shí)踐。

            顧名思義,單元測試面向的對象是單元,這個(gè)專有名詞源自編譯器領(lǐng)域的術(shù)語“編譯單元”。在面向過程中,指的是函數(shù),而在面向?qū)ο笾校傅耐ǔ>褪?#8220;類”。因而,每個(gè)功能類都應(yīng)該提供對應(yīng)的單元測試。

            實(shí)踐1:每個(gè)功能類都應(yīng)提供單元測試,且每一個(gè)測試類,只依賴于其要測試的受測類。使用偽造對象可以避免對其他類的依賴。

            解釋:保證一個(gè)測試類只關(guān)注一個(gè)被測類,當(dāng)測試不通過時(shí),就能迅速的定位到是誰發(fā)生了錯(cuò)誤,而不會(huì)受到其他類的干擾。

            簡單的數(shù)據(jù)類等可以不提供,但是要保證該測試的都要覆蓋到。并不存在一種合適的度量指標(biāo)可以量化地判斷某種單元測試方案是否成功。常用的標(biāo)準(zhǔn)(代碼覆蓋率和成功執(zhí)行的測試用例數(shù))都可以在受測軟件的質(zhì)量不變的情況下人為的修改(作假)。當(dāng)然,在無法確保程序員素質(zhì)的情況下,作為沒有辦法的辦法,使用這種標(biāo)準(zhǔn)也是可以的(或者無奈的說,必須的)。單元測試需要程序員自己把關(guān),關(guān)注哪些功能確實(shí)需要測試覆蓋。這也就是前面所說的一些程序員不相信單元測試可以提高生產(chǎn)率的理由--它更多的依賴于程序員的素質(zhì),這是沒有保證的。但同樣的,由于敏捷是一種以人為本的思想實(shí)踐,因而這種行為似乎又是一種必然。

            實(shí)踐1.1:使用偽造類避免對其他類的依賴。

            解釋:避免依賴的一種手段。

            例如,某個(gè)被測的方法聲明是這樣的:

            -(void)xxxx:(Person *)person;

            如果測試時(shí)傳入Person的話,就造成了測試類依賴于兩個(gè)類。當(dāng)由于person中的錯(cuò)誤引發(fā)測試不通過時(shí),就不能迅速的定位到受測類中是否有問題。遇到這種情況,就可以使用偽造類。假如方法中只使用了person的一個(gè)屬性name,那么可以將方法名重構(gòu)為

            -(void)xxxx:(id)person;(此處id有待商榷,只是這樣做最簡單)

            然后在單元測試的target中添加只包含name屬性的fakePerson來作為偽造類。這樣,一旦發(fā)生錯(cuò)誤就可以迅速的推測出錯(cuò)誤的來源。

            實(shí)踐1.2:使用偽造環(huán)境避免其他環(huán)境的干擾。

            解釋:適合于異步的方法測試。

            很經(jīng)常遇到的一種情況是測試有網(wǎng)絡(luò)環(huán)境的代碼。由于異步的存在,這會(huì)造成測試代碼不好寫。一種簡單的解決方法是,我們假定網(wǎng)絡(luò)一定是通暢的,則我們測試的代碼將分為兩部分,即拼裝發(fā)送功能和接收解析功能。假如發(fā)送和接收功能各自都能通過測試,那么我們大約可以確定這個(gè)異步方法的正確性。另一種方法是使用GHUnit,它支持異步代碼的測試。

            實(shí)踐2:測試用例(方法)名應(yīng)該是自解釋的且是獨(dú)立的。

            解釋:基本功。

            如果被測試類的名稱是XXX,那么測試類可以命名為XXXTests。而對于其中要測試的功能,命名應(yīng)該是自解釋的。這可以在發(fā)現(xiàn)錯(cuò)誤時(shí)盡快的定位問題所在。例如,如果某個(gè)屬性obj應(yīng)該是非空的,那么我們可以將其命名為:

            -(void)testObjNotNil{}

            每個(gè)方法目標(biāo)應(yīng)該是單一的,大多數(shù)情況下每個(gè)方法內(nèi)都只有一個(gè)斷言語句;方法不應(yīng)該依賴于其他方法的結(jié)果作為輸入,保證原子性。

          實(shí)踐3:斷言語句需要解釋測試者的意圖。

            解釋:基本功

            每種單元測試框架都提供了很多斷言語句,從根本上來說它們都是一樣的。但是測試者需要根據(jù)自己的目的選擇適當(dāng)?shù)恼Z句,這樣才可以讓別人閱讀測試代碼時(shí)理解用例設(shè)計(jì)的目的。例如對于STAssertNil和STAssertNotNil等等。

            實(shí)踐4:判斷某個(gè)意圖有沒有達(dá)到的很好的方法是檢測方法影響的數(shù)據(jù)有沒有合理的變化。

            解釋:基本功

            由于單元測試是使用斷言語句來做判斷的,因而最容易做的就是判斷數(shù)據(jù)的變化。這也就限定了單元測試能測試的方法范圍,即引起數(shù)據(jù)變化的方法。對于一些純展示的方法,例如播放一段特效,這種方法是無法靠單元測試來進(jìn)行約束的。測試數(shù)據(jù)的特性包括取值范圍(int、float等),排列順序(NSArray等),類型等等。

            實(shí)踐5:運(yùn)用重構(gòu)的手段使方法變得易于被測試。

            解釋:單元測試是保障重構(gòu)安全的手段,重構(gòu)也可以使代碼易于被測試。

            什么樣的代碼是容易進(jìn)行單元測試的?最簡單的一點(diǎn)就是,每個(gè)被測方法都應(yīng)該是功能單一的。當(dāng)然,這也是代碼規(guī)范中應(yīng)該做到的。方法的功能單一,則測試方法的斷言也會(huì)比較好確定。如果你發(fā)現(xiàn)某個(gè)方法很難進(jìn)行測試,則就應(yīng)該對這個(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é)任,以此來確定類具有的公有方法以及屬性。通過重構(gòu)將公有方法中的代碼轉(zhuǎn)化為私有方法,以使方法盡量短小緊湊。

            實(shí)踐6.1:應(yīng)對所有暴露的屬性和方法提供測試,私有方法則不必。

            解釋:如果運(yùn)用自上而下的方式構(gòu)建類,則理論上私有方法應(yīng)該都是公有方法重構(gòu)而得到的。實(shí)際上測試公有方法時(shí)這些私有方法都應(yīng)該被測試到了。而且,由于私有方法相對公有方法來說發(fā)生變動(dòng)的可能性很大,會(huì)造成不必要的修改測試代碼的成本。

            回調(diào)方法不屬于私有方法,也需要進(jìn)行測試。

            實(shí)踐6.2:回調(diào)方法的測試方法是直接調(diào)用。

            解釋:基本功

            由于回調(diào)方法一般是異步和不可觸發(fā)的(按正常流程),例如網(wǎng)絡(luò)事件的返回和按下按鈕的觸發(fā)事件。因而,測試的時(shí)候要直接調(diào)用來對其流程進(jìn)行檢測。例如某個(gè)按鈕的touch up inside事件:

            -(void)buttonPressed:(id)sender;

            可以根據(jù)方法中用到的方法、屬性偽造一個(gè)FakeButton按鈕作為參數(shù)傳遞進(jìn)行測試。



           實(shí)踐6.2:測試私有的方式,KVC、子類化和類別。

            解釋:基本功。

            遇到需要通過驗(yàn)證私有數(shù)據(jù)才能編寫的測試時(shí),可以考慮使用KVC和子類化。子類繼承于被測類,只包含于單元測試target,其作用就是在不該變受測類的情況下,使受測類具有某些易于被測的能力。

            實(shí)踐7:變化需要新測試的支持。

            解釋:保證測試的覆蓋度。

            就像敏捷中提到的“改變需要抽象”一樣,在測試中改變需要新的測試。當(dāng)然,度依然由程序員自己掌控。

            四、一般流程

            使用OCUnit最大的好處就是流程非常的簡單,簡單到讓你覺得非常愉悅。由于有XCode的支持,添加測試變得異常簡單。只要在新建工程時(shí)勾選“Include Unit Tests”,就會(huì)自動(dòng)的加入一個(gè)示例。然后再需要添加新的單元測試時(shí),新建一個(gè)“Objective-C test case class”就可以了。

            測試文件中,只要知道setUp是初始化的地方,tearDown是結(jié)束清理的地方,而且它們在每個(gè)用例方法執(zhí)行時(shí)都會(huì)重新執(zhí)行--這保證了測試用例的原子性。然后知道每個(gè)測試用例都是以test作為前綴的,并且無返回值。然后在方法中編寫斷言語句就可以了。輸入STAssertxxxxx就可以看到它們的聯(lián)想提示。編寫完成后,執(zhí)行菜單Product->Test,單元測試就完成了!

            五、測試驅(qū)動(dòng)(TDD)

            敏捷當(dāng)中提到了TDD這種開發(fā)方式。TDD的主旨是使開發(fā)者對其編寫的代碼更有信心,使開發(fā)者修改代碼時(shí)心里更加踏實(shí)。對于其總結(jié),還是引用原文比較妥當(dāng):“測試驅(qū)動(dòng)開發(fā)的妙處即在于,它以需求為引領(lǐng),通過測試的形式,來指導(dǎo)開發(fā)者進(jìn)行軟件的設(shè)計(jì)與架構(gòu),并編寫出最為精煉的代碼,使得測試用例運(yùn)行通過。經(jīng)過適當(dāng)?shù)闹貥?gòu)之后,測試用例與產(chǎn)品代碼可達(dá)到較為健康的狀態(tài)。”也就是上面提到的,通過自上而下的形式設(shè)計(jì)類,通過單元測試來不停地審視和重構(gòu)類,從而達(dá)到代碼的健康。

            如果在代碼寫完之后在編寫單元測試,那么就體現(xiàn)不出這種模式的好處了。這就好像寫完代碼再補(bǔ)文檔一樣,沒有什么意義。測試應(yīng)該在代碼開始之前,或者在代碼編寫中不停地進(jìn)行編寫更新,這樣才能使代碼不停進(jìn)步。這也正是TDD的意思。

            六、總結(jié)

            單元測試的代碼如此簡單,但是想寫好單元測試卻并不是一件簡單的事情。它需要程序員比較深的功底。由于個(gè)人水平所限,有一些東西說的比較啰嗦。把復(fù)雜問題簡單化是本事,任重而道遠(yuǎn)。希望大家可以在日常開發(fā)中運(yùn)用好這種簡潔高效的技術(shù)。

            最后,作為一個(gè)中國人,祈福雅安。網(wǎng)上有一些人在這種事件上還在噴。我想說的是,雖然這個(gè)世界是客觀存在的,但是每個(gè)人的世界觀確是主觀的。一個(gè)人觀察世界的角度決定了他認(rèn)為這個(gè)世界是什么樣的。中華民族的民族性格是內(nèi)斂的,在外在表現(xiàn)上似乎不如西方人張揚(yáng)。但是就像汶川一樣,全國人民在這種時(shí)刻表現(xiàn)出了我們這個(gè)民族內(nèi)在的優(yōu)秀品質(zhì)。老鼠屎在哪里都是老鼠屎,我以自己是中國人為榮。







          posted on 2013-05-16 10:42 順其自然EVO 閱讀(165) 評論(0)  編輯  收藏


          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導(dǎo)航:
           
          <2013年5月>
          2829301234
          567891011
          12131415161718
          19202122232425
          2627282930311
          2345678

          導(dǎo)航

          統(tǒng)計(jì)

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 旺苍县| 万安县| 平山县| 旬邑县| 筠连县| 崇文区| 仙桃市| 霍林郭勒市| 正镶白旗| 志丹县| 鸡西市| 荣成市| 绥阳县| 曲水县| 嘉峪关市| 武邑县| 孝昌县| 涟源市| 夏津县| 余姚市| 深圳市| 兴化市| 闸北区| 伊宁市| 柏乡县| 尖扎县| 泸州市| 桐庐县| 剑阁县| 专栏| 连州市| 高碑店市| 邛崃市| 中西区| 南昌县| 大竹县| 曲松县| 柏乡县| 沙河市| 汪清县| 宁津县|