上一篇文章簡單介紹了OCUnit和GHUnit兩款iOS開發中較為常見的單元測試框架,本文進一步介紹單元測試中的另一利器——匹配引擎(Matcher Engine)。匹配引擎可以替代斷言方法,配合單元測試引擎使用,測試用例可以更多樣化,更細致。
傳統斷言提供的方法數量和功能都有限,以導讀中提到的兩款框架為例,即使是斷言相對豐富的GHUnit也只是提供了38種斷言方法,范圍僅涵蓋了邏輯比較,異常和出錯等少數幾方面,仍然很單一。而使用匹配引擎代替斷言,可能性就大大豐富了,除了普通斷言支持的規則,一般的引擎還默認提供了包含,區間,繼承關系等。更重要的是,使用匹配引擎開發者可以自行開發匹配規則,引入與業務相關的邏輯判斷。
本文要介紹兩款匹配引擎,一款就是Hamcrest的Objective-C實現——OCHamcrest,另一款則是專為Objective-C/Cocoa而生的后來者——Expecta。接下來將結合GHUnitTest,介紹兩款匹配引擎如何在單元測試中發揮作用(有關GHUnitTest參考《iOS開發中的單元測試(一)》。
OCHamcrest
介紹匹配引擎必須要提Hamcrest,幾乎已經成為匹配引擎的代名詞。官網首頁上的一句話表明了它的身世:“Born in Java, Hamcrest now has implementations in a number of languages.”。這款誕生于Java的匹配引擎現在還支持除Java的Python、Ruby、PHP、Erlang和Objective-C。
加入工程
在iOS工程中使用OCHamcrest需要先獲取OCHamcrestIOS.framework,可以從Quality Coding直接下載,或在Github上獲取源碼編譯。注意:Github上托管的OCHamcrest工程以Submodule的形式關聯源代碼,因此如果使用命令行方式clone工程,需要執行“git submodule update --init”。
下載源碼后,進入Source目錄,執行MakeDistribution.sh腳本,將會在Source/build/Release下生成OCHamcrest.framework、OCHamcrestIOS.framework和OCHamcrest.framework.dSYM , OCHamcrestIOS.framework就是iOS工程中需要用到的框架,如圖1。

圖1,從源碼編譯生成 OCHamcrestIOS.framework
打開已經安裝了GHUnitTest的工程,把OCHamcrestIOS.framework添加到單元測試的Target中。在需要使用匹配引擎的用例中,定義“HC_SHORTHAND”并導入“<OCHamcrestIOS/OCHamcrestIOS.h>”(如圖2)。

圖2,把OCHamcrestIOS.framework導入工程
至此OCHamcrest已經安裝完成,可以再測試用例中使用匹配規則代替GHUnitTest的斷言方法。
預定義規則
OCHamcrest針對不同的數據類型提供了大量的預定義匹配規則,大大豐富了斷言的類型。支持的數據類型包括:對象、容器、數值和文本,此外還提供了專門的邏輯匹配規則。
以文本(一般就是NSString)為例,OCHamcrest提供了6種針對對象的匹配規則:
IsEqualIgnoringCase,該文本是否與給出的文本相同(忽略大小寫); IsEqualIgnoringWhiteSpace,該文本是否與給出的文本相同(忽略空格); StringContains,該文本是否包含給出的文本片段; StringContainsInOrder,該文本是否按照先后順序包含給出的若干文本片段; StringEndsWith,該文本是否以給出的文本片段結尾; StringStartsWith,該文本是否以給出的文本片段開頭。 |
另外,再舉OCHamcrest為對象(NSObject和NSObject的子類)預定義的8條規則:
ConformsToProtocol,該對象是否遵循了給出的協議,或者說是否實現了給出的Delegate; HasDescription,允許使用文本規則對給出的一段文本與該對象的描述進行匹配; HasProperty,該對象是否含有給出的屬性; IsInstanceOf,是給出的類的實例,或是給出的類子類的實例; IsTypeOf,是給出的類的實例,不同于IsInstanceOf,無法匹配子類實例; IsNil,為空; IsSame,與給出的對象是同一個實例。 |
撰寫用例
OCHamcrest提供了匹配規則和相應的斷言方法,配合單元測試框架(本文以GHUnit為例, 在《iOS開發中的單元測試(一)》中已經介紹了如何安裝GHUnit框架并撰寫用例)的驅動機制即可撰寫用例。本文以聯合使用上述提到的StringStartsWith和HasDescription規則為例。
首先,定義一個用于示例的類“Man”(如圖3),有屬性friends,當friends為空,其description為“Man without any friend, so sorry.”,反之為“Nice persion with [friends count] friend(s).”。(使用Foo或Bar這樣的示例會顯得很沒情懷吧 ;-|)

圖3,用于測試的類:Man
用例中判斷某Man實例的description是否以Nice開頭(這是不是一個友善的人),如圖4。

圖4,測試用例兩則
UntTestCase是GHTestCase的子類,引入<OCHamcrestIOS/OCHamcrestIOS.h>并定義HC_SHORTHAND表示使用OChamcrest。setUp方法在每個測試方法執行之前初始化一個Man實例;testANiceMan方法向Man實例的friends屬性中加入兩個值,因此該實例的description將返回“Nice man ....”;使用OCHamcrest提供的斷言方法assertThat與匹配規則配合,判斷該實例的description是否以“Nice”開頭;testNotANiceMan方法則直接測試一個未經過加入friends的實例測試。
上述測試,testANiceMan方法順利通過,testNotANiceMan不會通過,直接報出錯誤堆棧,并打印在匹配規則中預先定義好的出錯信息(如圖5)。

圖5,測試結果
輔助方法
Syntactic Sugar是一種提高匹配規則和斷言可讀性的方案,讓一個匹配和斷言看起來更像是一句自然語言的話,而非多個函數的堆砌,對實際的匹配運算不產生任何影響。例如,沒有加Sugar的匹配:
assertThat(foo, equalTo(bar)); |
加Sugar可以是:
assertThat(foo, is(equalTo(bar))); |
除了Sugar,OCHamcrest還提供了describedAs方法,用于輔助斷言方法,自定義出錯文案,例如:
assertThat(foo, describedAs(@”foo should be equal to bar”, equalTo(bar), nil)); |
自定義規則
OCHamcrest官方給出的自定義匹配規則示例是:onASaturday,判斷一個NSDateComponents實例是否為星期六。本文以上一節使用的Man對象為例,匹配某Man實例是否有一個名為“Joe”的好友,規則命名為:hasAFriendJoe。
自定義匹配規則包括兩部分,一個Macher類和一個用OBJC_EXPORT方式定義的函數。
自定義Macher類都是HCBaseMatcher的子類(如圖6),接口中定義的類初始化方法供匹配方法hasAFriendJoe調用,其實現則通過調用接口中定義的另一個實例方法。

圖6,HasAFriend接口和匹配方法hasAFriendJoe定義
在HasAFriend中需要引入<OCHamcrestIOS/HCDescription.h>,并重寫父類中的matches:和describeTo:方法(如圖7)。在maches:方法中實現匹配邏輯,匹配成功則返回YES,否則返回NO;describeTo是失敗后的描述;hasAFriendJoe方法只需要調用類方法初始化匹配規則類即可。匹配規則定義后,可以配合斷言方法使用,如上一節所示的assertThat方法:
assertThat(self.man, hasAFriendJoe());

圖7,規則實現
Expecta
Expecta專為Objective-C/Cocoa而生,相比OCHamcrest,其優化了匹配的語法,測試用例的可讀性更高。此外,Expecta對匹配對象類型沒有強制要求,允許任意類型的數據進行匹配。在OCHamcrest中每一條匹配規則都是一個方法,規則聯合使用也需要以參數形式傳遞。在Expecata中聯合規則的語法是以點號連接,借助Sugar介詞可以把一個聯合規則拼裝成一句符合自然語法的句子,例如:
OCHamcrest —— assertThat(@"foo", is(equalTo(@"foo"))); Expecta —— expect(@"foo").to.equal(@"foo"); |
· 加入工程
Expecta提供了CocoaPods的源,可以通過定義依賴引入: dependency 'Expecta', '~> 0.2.1' dependency 'Specta', '~> 0.1.7' # specta bdd framework |
或者從github上獲取源代碼,編譯出Library,引入XCode工程。下載源碼后,進入工程目錄,運行rake,編譯工程。編譯完成后,把products目錄拷貝到工程中(如圖8),在iOS/MacOSX工程中加入響應的.a文件(如圖9)。在Build Settings的Other Linker Flags中加入-ObjC參數(在《iOS開發中的單元測試(一)》中添加GHUnit一節介紹了如何添加-ObjC的參數)。與OCHamcrest類似,在測試用例中定義EXP_SHORTHAND,并引入“Expecta.h”(如圖10)。

圖8,加入編譯后的頭文件列表

圖9,加入Library文件

圖10,用例中引入Expecta.
· 預定義規則
Expecta提供的預定義規則只有20條,遠遠少于OCHamcrest提供的預定義規則。由于Expecta的匹配規則對匹配對象沒有要求,因此沒有提供像OCHamcrest中針對某種對象的特定規則。
在Expecta的github首頁可以看到全部預定義規則列表。舉幾個較有特點的規則為例:
expect(x).to.beCloseToWithin(y, z),x距離y的距離小于z expect(x).to.beTruthy(), x是否為真(或非空); expect(x).to.beFalsy(),x是否為假(或空/零); expect(^{ /* code */ }).to.raiseAny(),該Block是否拋出異常; expect(^{ /* code */ }).to.raise(@"ExceptionName"),該Block是否拋出給定名字異常。 |
此外,通過.notTo或.toNot對規則取反進行匹配,如:expect(x).notTo.equal(y)。
通過.will或.willNot進行異步匹配,即在超時時間(默認超時1秒,也可通過[Expecta setAsynchronousTestTimeout:x]設定)之前滿足匹配規則即可,如:expect(x).will.beNil()。
· 撰寫用例
Expecta不使用類似assertThat類似的輔助斷言方法,而是直接使用expecta.語法匹配。
仍然以GHUnit測試用例為例,測試一個數字n,是否在5附近,距離小于2,即處在[3,7]區間內(如圖11)。

圖11,Expecta測試用例
Expecta不支持匹配規則的聯合使用。
· 輔助方法
Expecta也有語法Sugar:to。
· 自定義規則
Expecta的自定義規則有兩種方式,靜態規則和動態規則。
定義靜態規則:
Expecta的匹配規則不是一個類,是通過框架提供的宏定義來實現的,操作比定義OCHamcrest規則簡單不少。仍以OCHamcrest中的判斷一個Man實例是否有名為“Joe”的好友。
通過EXPMatcherInterface()方法定義,該方法有兩個參數,規則名和規則參數列表。示例如圖12。

圖12,擴展規則hasAFriendJoe定義
EXPMacherInterface第二個參數允許通過這樣的方式定義列表:(NSString *Foo, int bar)。
在實現中,用EXPMatcherImplementationBegin和EXPMatcherImplementationEnd標示規則實現的頭尾。并定義prerequisite、match、failureMessageForTo和failureMessageForNotTo四個Block,分別返回與判斷結果,匹配結果,正向匹配出錯原因和反相匹配出錯原因(如圖13)。由于Expecta框架不支持ARC,因此需要在Build Settings中對該.m文件添加 -fno-objc-arc參數。在測試用例中可以通過如下語法調用:
expect(self.man).hasAFriend(@"Joe"); |
或反相匹配:
expect(self.man).notTo.hasAFriend(@"Joe"); |

圖13,Expecta自定義規則實現
定義動態規則:
動態規則是本質上并不是一段邏輯匹配,而是通過Expecta的語法對匹配對象的屬性進行是否為真的斷言。例如:
@interfaceLightSwitch:NSObject @property(nonatomic,assign,getter=isTurnedOn)BOOLturnedOn; @end @implementationLightSwitch @synthesizeturnedOn; @end |
可以寫出如下斷言:
建立動態規則:
EXPMatcherInterface(isTurnedOn, (void)); |
就可以通過以下斷言判斷turendOn屬性的真假:
expect(lightSwitch).isTurnedOn(); |
總結
整體看兩款匹配引擎,Expecta小巧,敏捷,提供了多種靈活的匹配方式,OCHamcrest從Hamecrest體系繼承而來,形式更加中庸,提供的機制更完善。從開發者的角度看,Expecta更好玩,而OCHamcrest更實用,在實驗性的項目中我會偏向選擇Expecta,而較正式的項目則會使用OCHamcrest。
OCHamcrest結合上一篇《iOS開發中的單元測試(一)》中介紹的單元測試框架GHUnit可以給開發者提供一個完整的單元測試方案,建議開發者在自己的項目中引入這樣的質量自控機制,寫出健壯的代碼。
通過兩篇文章介紹了單元測試框架和匹配引擎的一些基礎知識,在接下來的文章中,我將結合一個項目,從實戰角度詳細記述如何開發帶有單元測試的iOS項目。
相關文章:
iOS開發中的單元測試(一)