標籤:
圖前內容轉載自: slegetank
開始之前
本文側重講述如何在iOS程式的開發過程中使用單元測試。使用Xcode內建的OCUnit作為測試架構。
一、單元測試概述
單 元測試作為敏捷開發實踐的組成之一,其目的是提高軟體開發的效率,維持代碼的健康性。其目標是證明軟體能夠正常運行,而不是發現bug(發現 bug這一目的與開發成本是正相關的,雖然發現bug是保證軟體品質的一種手段,但是很顯然這與降低軟體開發成本這一目的背道而馳)。它是對軟體品質的一 種保證,例如重構之後我們需要保證軟體產品的正常運行。
很多人認為編寫單元測試沒有用是認為單元測試並不能保證一定能減少bug發生的幾 率,而由於編寫單元測試一定會花費一定的時間與精力,因而必然的會 增加成本。客觀的說,造成這種原因很大的程度上是程式員的水平不夠高。我認為使用使用單元測試帶來巨大好處的必要條件如下所示:
- 程式員本身的編程水平--是否有較多的代碼經驗,是否熟練掌握重構
- 程式員對項目的認知--是否能正確理解軟體或模組的需求
- 項目品質--是否穩定,是否長期多版本,是否需要應對較多變化
如果程式員水平較高,對需求理解較為清晰,項目需要面對較多的變化,那麼毫無疑問單元測試對於軟體非常有益。假如軟體功能簡單且開發週期短,不需要進行複雜的維護工作,那麼單元測試的意義並不大。
優秀的單元測試實踐的好處:
- 好的單元測試就是一份好的文檔,並且比文檔更能為程式員所接受,它直接描述了測試員對受測代碼的結果所持的預期。
- 當代碼由別人維護時(或自己進行重構時),通過單元測試的約束,才能保證在加入新功能或修改舊功能時代碼的正確性。
- 由於單元測試的自動化執行,保證了在整個開發流程中代碼都會被測試,這非常符合XP思想。
- 保證在面對軟體功能的變化時,程式員可以較為放心的進行代碼重構,而不必擔心是否破壞了原有功能。
- 好的單元測試可以降低bug數量,而對於專案管理來說,修改bug這個過程是無法制定計劃的,可以使軟體的開發流程更容易掌控。
- 可以由老程式員編寫描述某個類行為的測試,以此指導新程式員對類的編碼。
- ……
好處還有很多,但最重要的一點就是保證了軟體品質的同時,由於減少bug和應對變化造成的迴歸bug的產生等,提高了勞動生產率。而且,在敏捷流程中,使用單元測試是必須掌握的手段,否則就沒發保證重構的正確性,從而造成代碼無法面對變化。
二、iOS的單元測試概述
剛接觸用戶端編程時,我在很長一段時間內都想不通對於用戶端程式如何編寫單元測試。單元測試本質上說白了就是用一些斷言來判定結果,而這種方式是如何應用到具有複雜互動的介面測試上來的呢?
我們要做的就是將用戶端代碼轉化為易於測試的代碼。什麼樣的代碼易於測試呢?它至少是這樣的:
1、被測方法需要產生可測量的結果。
2、類之間的關係應該是松耦合的。
其中第一條是必要條件。使用斷言這種形式指明了測試的方法最終要造成某些可以度量的結果。因而,我們需要盡量的將展示和商務邏輯分離開來。展示的代碼是沒法測試的,例如有的方法只是播放動畫。而商務邏輯最終都會造成一些資料的改變,這是容易測試的。
大略的講,作為一個iOS程式員來說,首先要瞭解一個叫做MVC的模式。這個模式定義了Cocoa Touch架構的總體結構。在iOS程式中,我們也需要按照這種模式進行介面代碼的編寫。這樣設計出來的類具有較好的結構,且比較適合於做單元測試。
然後一定要懂得不停重構代碼,這樣我們才能使代碼不停地改善,不停地變得更加適合單元測試。
有一些架構可以協助大家更好的測試,分別是OCUnit、GTM、GHUnit、CATCH、OCMock,但目前對我來說,OCUnit足夠用了。作為蘋果官方提供的測試架構,它最大的優點就是簡單易用。
三、單元測試實踐
下面是一些我所理解的單元測試中比較好的實踐。
顧名思義,單元測試面向的對象是單元,這個專有名詞源自編譯器領域的術語“編譯單元”。在面向過程中,指的是函數,而在物件導向中,指的通常就是“類”。因而,每個功能類都應該提供對應的單元測試。
實踐1 每個功能類都應提供單元測試,且每一個測試類別,只依賴於其要測試的受測類。使用偽造對象可以避免對其他類的依賴。
解釋 保證一個測試類別只關注一個被測類,當測試不通過時,就能迅速的定位到是誰發生了錯誤,而不會受到其他類的幹擾。
簡 單的資料類等可以不提供,但是要保證該測試的都要覆蓋到。並不存在一種合適的度量指標可以量化地判斷某種單元測試方案是否成功。常用的標準(代碼 覆蓋率和成功執行的測試案例數)都可以在受測軟體的品質不變的情況下人為的修改(作假)。當然,在無法確保程式員素質的情況下,作為沒有辦法的辦法,使用 這種標準也是可以的(或者無奈的說,必須的)。單元測試需要程式員自己把關,關注哪些功能確實需要測試覆蓋。這也就是前面所說的一些程式員不相信單元測試 可以提高生產率的理由--它更多的依賴於程式員的素質,這是沒有保證的。但同樣的,由于敏捷是一種以人為本的思想實踐,因而這種行為似乎又是一種必然。
實踐1.1 使用偽造類避免對其他類的依賴。
解釋 避免依賴的一種手段。
例如,某個被測的方法聲明是這樣的:
-(void)xxxx:(Person *)person;
如果測試時傳入Person的話,就造成了測試類別依賴於兩個類。當由於person中的錯誤引發測試不通過時,就不能迅速的定位到受測類中是否有問題。遇到這種情況,就可以使用偽造類。假如方法中只使用了person的一個屬性name,那麼可以將方法名重構為
-(void)xxxx:(id)person;(此處id有待商榷,只是這樣做最簡單)
然後在單元測試的target中添加只包含name屬性的fakePerson來作為偽造類。這樣,一旦發生錯誤就可以迅速的推測出錯誤的來源。
實踐1.2 使用偽造環境避免其他環境的幹擾。
解釋 適合於非同步方法測試。
很 經常遇到的一種情況是測試有網路環境的代碼。由於非同步存在,這會造成測試代碼不好寫。一種簡單的解決方案是,我們假定網路一定是通暢的,則我們 測試的代碼將分為兩部分,即拼裝發送功能和接收解析功能。假如發送和接收功能各自都能通過測試,那麼我們大約可以確定這個非同步方法呼叫的正確性。另一種方法是 使用GHUnit,它支援非同步代碼的測試。
實踐2 測試案例(方法)名應該是自解釋的且是獨立的。
解釋 基本功。
如果被測試類別的名稱是XXX,那麼測試類別可以命名為XXXTests。而對於其中要測試的功能,命名應該是自解釋的。這可以在發現錯誤時儘快的定位問題所在。例如,如果某個屬性obj應該是非空的,那麼我們可以將其命名為:
-(void)testObjNotNil{}
每個方法目標應該是單一的,大多數情況下每個方法內都只有一個Assert 陳述式;方法不應該依賴於其他方法的結果作為輸入,保證原子性。
實踐3 Assert 陳述式需要解釋測試者的意圖。
解釋 基本功
每種單元測試架構都提供了很多Assert 陳述式,從根本上來說它們都是一樣的。但是測試者需要根據自己的目的選擇適當的語句,這樣才可以讓別人閱讀測試代碼時理解用例設計的目的。例如對於STAssertNil和STAssertNotNil等等。
實踐4 判斷某個意圖有沒有達到的很好的方法是檢測方法影響的資料有沒有合理的變化。
解釋 基本功
由於單元測試是使用Assert 陳述式來做判斷的,因而最容易做的就是判斷資料的變化。這也就限定了單元測試能測試的方法範圍,即引起資料變化的方法。對於一些純展示的方法,例如播放一段特效,這種方法是無法靠單元測試來進行約束的。測試資料的特性包括取值範圍(int、float等),排列順序(NSArray等),類型等等。
實踐5 運用重構的手段使方法變得易於被測試。
解釋 單元測試是保障重構安全的手段,重構也可以使代碼易於被測試。
什麼樣的代碼是容易進行單元測試的?最簡單的一點就是,每個被測方法都應該是功能單一的。當然,這也是代碼規範中應該做到的。方法的功能單一,則測試方法的斷言也會比較好確定。如果你發現某個方法很難進行測試,則就應該對這個方法進行拆分重構。
實踐5.1 面向抽象設計類之間的關係。
解釋 利於偽造類的實現。
類之間通訊如果依賴於抽象(介面),則可以較容易的使用偽造類。參照實踐1.1。
實踐6 運用自上而下的方式構建類。
解釋 自上而下的方式可以使類的功能明確,類的構成將會清晰緊湊,不會出現一些廢方法。
先確定類需要負擔的責任,以此來確定類具有的公有方法以及屬性。通過重構將公有方法中的代碼轉化為私人方法,以使方法盡量短小緊湊。
實踐6.1 應對所有暴露的屬性和方法提供測試,私人方法則不必。
解釋 如果運用自上而下的方式構建類,則理論上私人方法應該都是公有方法重構而得到的。實際上測試公有方法時這些私人方法都應該被測試到了。而且,由於私人方法相對公有方法來說發生變動的可能性很大,會造成不必要的修改測試代碼的成本。
回調方法不屬於私人方法,也需要進行測試。
實踐6.2 回調方法的測試方法是直接調用。
解釋 基本功
由於回調方法一般是非同步和不可觸發的(按正常流程),例如網路事件的返回和按下按鈕的觸發事件。因而,測試的時候要直接調用來對其流程進行檢測。例如某個按鈕的touch up inside事件:
-(void)buttonPressed:(id)sender;
可以根據方法中用到的方法、屬性偽造一個FakeButton按鈕作為參數傳遞進行測試。
實踐6.2 測試私人的方式,KVC、子類化和類別。
解釋 基本功。
遇到需要通過驗證私人資料才能編寫的測試時,可以考慮使用KVC和子類化。子類繼承於被測類,只包含於單元測試target,其作用就是在不該變受測類的情況下,使受測類具有某些易於被測的能力。
實踐7:變化需要新測試的支援。
解釋:保證測試的覆蓋度。
就像敏捷中提到的“改變需要抽象”一樣,在測試中改變需要新的測試。當然,度依然由程式員自己掌控。
四、一般流程
使 用OCUnit最大的好處就是流程非常的簡單,簡單到讓你覺得非常愉悅。由於有XCode的支援,添加測試變得異常簡單。只要在建立工程時勾選 “Include Unit Tests”,就會自動的加入一個樣本。然後再需要添加新的單元測試時,建立一個“Objective-C test case class”就可以了。
測試檔案中,只要知道setUp是初始化的地方,tearDown是結束清理的地方,而且它們在每個用例 方法執行時都會重新執行--這保證了測試用 例的原子性。然後知道每個測試案例都是以test作為首碼的,並且無傳回值。然後在方法中編寫Assert 陳述式就可以了。輸入STAssertxxxxx就可以看 到它們的聯想提示。編寫完成後,執行菜單Product->Test,單元測試就完成了!
五、測試驅動(TDD)
敏 捷當中提到了TDD這種開發方式。TDD的主旨是使開發人員對其編寫的代碼更有信心,使開發人員修改代碼時心裡更加踏實。對於其總結,還是引用原文比 較妥當:“測試驅動開發的妙處即在於,它以需求為引領,通過測試的形式,來指導開發人員進行軟體的設計與架構,並編寫出最為精鍊的代碼,使得測試案例運行通 過。經過適當的重構之後,測試案例與產品代碼可達到較為健康的狀態。”也就是上面提到的,通過自上而下的形式設計類,通過單元測試來不停地審視和重構類, 從而達到代碼的健康。
如果在代碼寫完之後在編寫單元測試,那麼就體現不出這種模式的好處了。這就好像寫完代碼再補文檔一樣,沒有什麼意義。測試應該在代碼開始之前,或者在代碼編寫中不停地進行編寫更新,這樣才能使代碼不停進步。這也正是TDD的意思。
將要對方法:+(instancetype)cellWithTableView:(UITableView *)tableView;進行測試
測試方法為:- (void)testExample
點擊左側小方塊進行測試,通過為綠色,未通過未紅色。
自己建立單元測試類
單元測試優點:
1.測試代碼和業務代碼分離,分別屬於兩個不同的target,程式發布時,測試代碼不會被打包進最終程式,這樣避免了在業務代碼中寫很多NSLog之類的垃圾代碼。
2.直觀。直接利用斷言特點看最終結果是紅還是綠,比NSLog更易讀。
3.速度快。不需要啟動完整應用程式。
4.分離性好。項目未完成,無法完整啟動時,也可以進行測試。
iOS單元測試