Kiwi 是一個適用于iOS開發的行為驅動測試框架,旨在提供一個足夠簡單易用的BDD庫.
target :AmazingAPPTests, :exclusive => true do pod 'Kiwi'end
把 AmazingAppTests 改為你自己的工程中的Tests target的名字,比如我的是 iOS122Tests,然后更新即可:
pod update --verbose --no-repo-update
為了快速測試Kiwi是否安裝成功,你可以用下面的代碼替換到你的 Tests目錄下已有的文件中的默認內容,然后點擊Xcode導航欄 PRoduct->Test(或者使用快捷鍵 cmd + u),此時如果提示你 Test Failed,點擊錯誤提示,會在左側第四導航欄看到類似下面的錯誤:
Assertions: 'Math, is pretty cool' [FAILED], expected subject to equal (KWValue) 43, got (KWValue) 42File: MathSpec.m:9
如果不能看到上述錯誤信息,說明你的工程配置可能有問題,可以參考這里詳細微調下: Getting Started with Kiwi 2.0
#import "Kiwi.h"
導入Kiwi庫.這應該在規則的文件開始處最先導入.SPEC_BEGIN(ClassName)
和 SPEC_END
宏,用于標記 KWSpec 類的開始和結束,以及測試用例的分組聲明.registerMatchers(aNamespacePrefix)
注冊所有使用指定命名空間前綴的匹配器.除了Kiwi默認的匹配器,這些匹配器也可以在當前規則中使用.describe(aString, aBlock)
開啟一個上下文環境,可包含測試用例或嵌套其他的上下文環境.beforeAll(aBlock)
在所有內嵌上下文或當前上下文的`it
block執行之前執行一次.afterAll(aBlock)
在所有內嵌上下文或當前上下文的`it
block執行之后執行一次.beforeEach(aBlock)
在所有包含的上下文環境的 it
block執行之前,均各執行一次.用于初始化指定上下文環境的代碼,應該放在這里.afterEach(aBlock)
在所有包含的上下文環境的 it
block執行之后,均各執行一次.it(aString, aBlock)
聲明一個測試用例.這里描述了對對象或行為的期望.specify(aBlock)
聲明一個沒有描述的測試用例.這個常用于簡單的期望.pending(aString, aBlock)
可用于標記尚未完成的功能或用例,僅會使Xcode輸出一個黃色警告.(有點TODO的趕腳)let(subject, aBlock)
聲明一個本地工具變量,這個變量會在規則內所有上下文的每個 it
block執行前,重新初始化一次.#import "Kiwi.h"#import "YFKiwiSample.h"SPEC_BEGIN(SpecName)describe(@"ClassName", ^{ registerMatchers(@"BG"); // 注冊 BGTangentMatcher, BGConvexMatcher 等. context(@"a state the component is in", ^{ let(variable, ^{ // 在每個包含的 "it" 執行前執行執行一次. return [[YFKiwiSample alloc]init]; }); beforeAll(^{ // 執行一次 NSLog(@"beforAll"); }); afterAll(^{ // Occurs once NSLog(@"afterAll"); }); beforeEach(^{ // 在每個包含的 "it" 執行前,都執行一次. NSLog(@"beforeEach"); }); afterEach(^{ // 在每個包含的 "it" 執行后,都執行一次. NSLog(@"afterEach"); }); it(@"should do something", ^{ NSLog(@"should do something");// [[variable should] meetSomeExpectation]; }); specify(^{ NSLog(@"specify"); [[variable shouldNot] beNil]; }); context(@"inner context", ^{ NSLog(@"inner context"); it(@"does another thing", ^{ NSLog(@"does another thing"); }); pending(@"等待實現的東西", ^{ NSLog(@"等待實現的東西"); }); }); });});SPEC_END
期望,用來驗證用例中的對象行為是否符合你的語氣.一個期望,具有如下形式: [[subject should] someCondition:anArgument]
.此處 [subject should]
是表達式的類型, ... someCondition:anArgument]
是匹配器的表達式.
示例:
// 可以用下面的內容替換原來的tests.m中的內容,然后cmd+u// ;測試失敗可自行解決;解決不了的,繼續往下看.#import "Kiwi.h"#import "YFKiwiCar.h"SPEC_BEGIN(CarSpec)describe(@"YFKiwiCar", ^{ it(@"A Car Rule", ^{ id car = [YFKiwiCar new]; [[car shouldNot] beNil]; [[car should] beKindOfClass:[YFKiwiCar class]]; [[car shouldNot] conformToProtocol:@protocol(NSCopying)]; [[[car should] have:4] wheels]; [[theValue([(YFKiwiCar *)car speed]) should] equal:theValue(42.0f)]; [[car should] receive:@selector(changeToGear:) withArguments: theValue(3)]; [car changeToGear: 3]; });});SPEC_END
[subject should]
和 [subject shouldNot]
表達式,類似于一個接收器,用于接收一個期望匹配器.他們后面緊跟的是真實的匹配表達式,這些表達式將真正被用于計算.
默認地,主語守衛(一種機制,可以保證nil不引起崩潰)也會在[subject should ]
和 [subject shouldNot]
被使用時創建.給 nil
發送消息,通常不會有任何副作用.但是,你幾乎不會希望:一個表達式,只是為了給某個對象傳遞一個無足輕重的消息,就因為對象本身是nil.也就說,向nil
對象本身發送消息,并不會有任何副作用;但是在BBD里,某個要被傳遞消息的對象是nil
,通常是非預期行為.所以,這些表達式的對象守衛機制,會將左側無法判定為不為nil
的表達式判定為 fail
失敗.
"裝箱"是固定術語譯法,其實即使我們iOS常說的基本類型轉NSObject類型(事實如此,勿噴).
部分表達式中,匹配器表達式的參數總是NSObject對象.當將一個標量(如int整型,float浮點型等)用于需要id
類型參數的地方時,應使用theValue(一個標量)
宏將標量裝箱.這種機制也適用于: 當一個標量需要是一個表達式的主語(主謂賓,基本語法規則,請自行腦補)時,或者一個 存根
的值需要是一個標量時.
示例:
[[theValue(1 + 1) should] equal:theValue(2)];[[theValue(YES) shouldNot] equal:theValue(NO)];[[theValue(20u) should] beBetween:theValue(1) and:theValue(30.0)]; YFKiwiCar * car = [YFKiwiCar new];[[theValue(car.speed) should] beGreaterThan:theValue(40.0f)];
在iOS中,常將調用某個實例對象的方法成為給這個對象發送了某個消息.所以"消息模式"中的"消息",更多的指的的實例對象的方法;"消息模式"也就被用來判斷對象的某個方法是否會調用以及是否會按照預期的方式調用.
一些 Kiwi 匹配器支持使用消息模式的期望.消息模式部分,常被放在一個表達式的后部,就像一個將要發給主語的消息一樣.
示例:
YFKiwiCar * cruiser = [[YFKiwiCar alloc]init]; [[cruiser should] receive:@selector(jumpToStarSystemWithIndex:) withArguments: theValue(3)]; [cruiser jumpToStarSystemWithIndex: 3];
[[subject shouldNot] beNil]
[[subject should] beNil]
[[subject should] beIdenticalTo:(id)anObject]
- 比較是否完全相同[[subject should] equal:(id)anObject]
[[subject should] equal:(double)aValue withDelta:(double)aDelta]
[[subject should] beWithin:(id)aDistance of:(id)aValue]
[[subject should] beLessThan:(id)aValue]
[[subject should] beLessThanOrEqualTo:(id)aValue]
[[subject should] beGreaterThan:(id)aValue]
[[subject should] beGreaterThanOrEqualTo:(id)aValue]
[[subject should] beBetween:(id)aLowerEndpoint and:(id)anUpperEndpoint]
[[subject should] beInTheIntervalFrom:(id)aLowerEndpoint to:(id)anUpperEndpoint]
[[subject should] beTrue]
[[subject should] beFalse]
[[subject should] beYes]
[[subject should] beNo]
[[subject should] beZero]
[[subject should] containString:(NSString*)substring]
[[subject should] containString:(NSString*)substring options:(NSStringCompareOptions)options]
[[subject should] startWithString:(NSString*)prefix]
[[subject should] endWithString:(NSString*)suffix]
示例:
[[@"Hello, world!" should] containString:@"world"]; [[@"Hello, world!" should] containString:@"WORLD" options:NSCaseInsensitiveSearch]; [[@"Hello, world!" should] startWithString:@"Hello,"]; [[@"Hello, world!" should] endWithString:@"world!"];
[[subject should] matchPattern:(NSString*)pattern]
[[subject should] matchPattern:(NSString*)pattern options:(NSRegularExpressionOptions)options]
[[@"ababab" should] matchPattern:@"(ab)+"]; [[@" foo " shouldNot] matchPattern:@"^foo$"]; [[@"abABab" should] matchPattern:@"(ab)+" options:NSRegularExpressionCaseInsensitive];
[[theBlock(^{ ... }) should] change:^{ return (NSInteger)count; }]
[[theBlock(^{ ... }) should] change:^{ return (NSInteger)count; } by:+1]
[[theBlock(^{ ... }) should] change:^{ return (NSInteger)count; } by:-1]
示例:
it(@"Expectations: Count changes", ^{ NSMutableArray * array = [NSMutableArray arrayWithCapacity: 42]; [[theBlock(^{ [array addObject:@"foo"]; }) should] change:^{ return (NSInteger)[array count]; } by:+1]; [[theBlock(^{ [array addObject:@"bar"]; [array removeObject:@"foo"]; }) shouldNot] change:^{ return (NSInteger)[array count]; }]; [[theBlock(^{ [array removeObject:@"bar"]; }) should] change:^{ return (NSInteger)[array count]; } by:-1]; });
[[subject should] beKindOfClass:(Class)aClass]
[[subject should] beMemberOfClass:(Class)aClass]
[[subject should] conformToProtocol:(Protocol *)aProtocol]
[[subject should] respondToSelector:(SEL)aSelector]
對于集合主語(即,主語是集合類型的):
[[subject should] beEmpty]
[[subject should] contain:(id)anObject]
[[subject should] containObjectsInArray:(NSArray *)anArray]
[[subject should] containObjects:(id)firstObject, ...]
[[subject should] haveCountOf:(NSUInteger)aCount]
[[subject should] haveCountOfAtLeast:(NSUInteger)aCount]
[[subject should] haveCountOfAtMost:(NSUInteger)aCount]
對于集合鍵(即此屬性/方法名對應/返回一個集合類型的對象):
[[[subject should] have:(NSUInteger)aCount] collectionKey]
[[[subject should] haveAtLeast:(NSUInteger)aCount] collectionKey]
[[[subject should] haveAtMost:(NSUInteger)aCount] collectionKey]
如果主語是一個集合(比如 NSArray數組), coollectionKey
可以是任何東西(比如 items
),只要遵循語法結構就行.否則, coollectionKey
應當是一個可以發送給主語并返回集合類型數據的消息.
更進一步說: 對于集合類型的主語,coollectionKey
的數量總是根據主語的集合內的元素數量, coollectionKey
本身并無實際意義.
示例:
NSArray *array = [NSArray arrayWithObject:@"foo"]; [[array should] have:1] item]; Car *car = [Car car]; [car setPassengers:[NSArray arrayWithObjects:@"Eric", "Stan", nil]]; [[[[car passengers] should] haveAtLeast:2] items]; [[[car should] haveAtLeast:2] passengers];
這些期望用于驗證主語是否在從創建期望到用例結束的這段時間里接收到了某個消息(或者說對象的某個方法是否被調用).這個期望會同時存儲 選擇器或參數等信息,并依次來決定期望是否滿足.
這些期望可用于真實或模擬的獨享,但是在設置 receive
表達式時,Xcode 可能會給警告(報黃).
對參數無要求的選擇器:
[[subject should] receive:(SEL)aSelector]
[[subject should] receive:(SEL)aSelector withCount:(NSUInteger)aCount]
[[subject should] receive:(SEL)aSelector withCountAtLeast:(NSUInteger)aCount]
[[subject should] receive:(SEL)aSelector withCountAtMost:(NSUInteger)aCount]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCount:(NSUInteger)aCount]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCountAtLeast:(NSUInteger)aCount]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCountAtMost:(NSUInteger)aCount]
含有指定參數的選擇器:
[[subject should] receive:(SEL)aSelector withArguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector withCount:(NSUInteger)aCount arguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector withCountAtLeast:(NSUInteger)aCount arguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector withCountAtMost:(NSUInteger)aCount arguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withArguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCount:(NSUInteger)aCount arguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCountAtLeast:(NSUInteger)aCount arguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCountAtMost:(NSUInteger)aCount arguments:(id)firstArgument, ...]
示例:
subject = [Cruiser cruiser];[[subject should] receive:@selector(energyLevelInWarpCore:) andReturn:theValue(42.0f) withCount:2 arguments:theValue(7)];[subject energyLevelInWarpCore:7];float energyLevel = [subject energyLevelInWarpCore:7];[[theValue(energyLevel) should] equal:theValue(42.0f)];
注意你可以將 any()
通配符用作參數.如果你只關心一個方法的部分參數的值,這回很有用:
id subject = [Robot robot];[[subject should] receive:@selector(speak:afterDelay:whenDone:) withArguments:@"Hello world",any(),any()];[subject speak:@"Hello world" afterDelay:3 whenDone:nil];
[[@"MyNotification" should] bePosted];
[[@"MyNotification" should] bePostedWithObject:(id)object];
[[@"MyNotification" should] bePostedWithUserInfo:(NSDictionary *)userInfo];
[[@"MyNotification" should] bePostedWithObject:(id)object andUserInfo:(NSDictionary *)userInfo];
[[@"MyNotification" should] bePostedEvaluatingBlock:^(NSNotification *note)block];
Example:
it(@"Notification", ^{ [[@"自定義通知" should] bePosted]; NSNotification *myNotification = [NSNotification notificationWithName:@"自定義通知" object:nil]; [[NSNotificationCenter defaultCenter] postNotification:myNotification];});
[[subject shouldEventually] receive:(SEL)aSelector]
[[subject shouldEventually] receive:(SEL)aSelector withArguments:(id)firstArgument, ...]
[[theBlock(^{ ... }) should] raise]
[[theBlock(^{ ... }) should] raiseWithName:]
[[theBlock(^{ ... }) should] raiseWithReason:(NSString *)aReason]
[[theBlock(^{ ... }) should] raiseWithName:(NSString *)aName reason:(NSString *)aReason]
示例:
[[theBlock(^{ [NSException raise:@"FooException" reason:@"Bar-ed"]; }) should] raiseWithName:@"FooException" reason:@"Bar-ed"];
Kiwi中,自定義匹配器的最簡單方式是創建KWMatcher的子類,并以適當的方式重寫下面示例中的方法.
為了讓你自定義的匹配器在規則中可用,你需要在規則中使用 registerMatchers(namespacePrefix)
進行注冊.
看下Kiwi源文件中的匹配器寫法(如KWEqualMatcher等),將會使你受益匪淺.
示例:
// Snippet from AnimalTypeMatcher.m #pragma mark Getting Matcher Strings // REQUIRED: Return an array of selector strings for the expectations this // matcher is used for. // // For example, this matcher handles [[subject should] beTypeOfMammal:] and // [[subject should] beTypeOfInsect:]. + (NSArray *)matcherStrings { return [NSArray arrayWithObjects:@"beTypeOfMammal:", @"beTypeOfInsect:", nil]; } #pragma mark Matching // REQUIRED: Evaluate the predicate here. // self.subject is available automatically. // self.otherSubject is a member variable you would have declared yourself. - (BOOL)evaluate { return [[self.subject animalType] isEqual:self.otherSubject]; } #pragma mark Getting Failure Messages // REQUIRED: Return a custom error message for when "should" is used. - (NSString *)failureMessageForShould { return @"expected subject to be an animal or insect"; } // OPTIONAL: If you don't override this, Kiwi uses -failureMessageForShould: and // replaces the first "to" with "not to". - (NSString *)failureMessageForShouldNot { return @"expected subject not to be an animal or insect"; } #pragma mark Configuring Matchers // These methods should correspond to the selector strings returned in +matcherStrings. // // Use them to finish configuring your matcher so that -evaluate can be called // successfully later. Being a subclass of KWMatcher handles other details like // setting up self.subject. - (void)beTypeOfMammal:(id)anObject { self.otherSubject = anObject; } - (void)beTypeOfInsect:(id)anObject { self.otherSubject = anObject; }
模擬對象模擬某個類,或者遵循某個寫一個.他們讓你在完全功能完全實現之前,就能更好地專注于對象間的交互行為,并且能降低對象間的依賴--模擬或比避免那些運行規則時幾乎很難出現的情況.
it(@"Mock", ^{ id carMock = [YFKiwiCar mock]; [ [carMock should] beMemberOfClass:[YFKiwiCar class]]; [ [carMock should] receive:@selector(currentGear) andReturn:theValue(3)]; [ [theValue([carMock currentGear]) should] equal:theValue(3)]; id carNullMock = [YFKiwiCar nullMock]; [ [theValue([carNullMock currentGear]) should] equal:theValue(0)]; [carNullMock applyBrakes]; id flyerMock = [KWMock mockForProtocol:@protocol(YFKiwiFlyingMachine)]; [ [flyerMock should] conformToProtocol:@protocol(YFKiwiFlyingMachine)]; [flyerMock stub:@selector(dragCoefficient) andReturn:theValue(17.0f)]; id flyerNullMock = [KWMock nullMockForProtocol:@protocol(YFKiwiFlyingMachine)]; [flyerNullMock takeOff];});
通常模擬對象收到一個非預期的選擇器或消息模式時,會拋出異常(PS:iOS開發常見錯誤奔潰之一).在模擬對象上使用 stub
或 receive
期望,期望的消息會自動添加到模擬對象上,以實現對方法的模擬.
如果你不關心模擬對象如何處理其他非預期的消息,也不想在收到非預期消息時拋出異常,那就使用 null 模擬對象吧(也即 null 對象).
創建類的模擬實例(NSObject 擴展):
[SomeClass mock]
[SomeClass mockWithName:(NSString *)aName]
[SomeClass nullMock]
[SomeClass nullMockWithName:(NSString *)aName]
創建類的模擬實例:
[KWMock mockForClass:(Class)aClass]
[KWMock mockWithName:(NSString *)aName forClass:(Class)aClass]
[KWMock nullMockForClass:(Class)aClass]
[KWMock nullMockWithName:(NSString *)aName forClass:(Class)aClass]
創建遵循某協議的實例:
[KWMock mockForProtocol:(Protocol *)aProtocol]
[KWMock mockWithName:(NSString *)aName forProtocol:(Protocol *)aProtocol]
[KWMock nullMockForProtocol:(Protocol *)aProtocol]
[KWMock nullMockWithName:(NSString *)aName forProtocol:(Protocol *)aProtocol]
存根,能返回指定定選擇器或消息模式的封裝好的請求.Kiwi中,你可以存根真實對象(包括類對象)或模擬對象的方法.沒有指定返回值的存根,將會對應返回nil,0等零值.存根需要返回標量的,標量需要使用 theValue(某個標量)
宏 裝箱.
所有的存根都會在規范的一個例子的末尾(一個it
block)被清除.
存根選擇器:
[subject stub:(SEL)aSelector]
[subject stub:(SEL)aSelector andReturn:(id)aValue]
存根消息模式:
[ [subject stub] *messagePattern*]
[ [subject stubAndReturn:(id)aValue] *messagePattern*]
示例:
id cruiser = [Cruiser cruiser]; [ [cruiser stubAndReturn:theValue(42.0f)] energyLevelInWarpCore:7]; float energyLevel = [cruiser energyLevelInWarpCore:7]; [ [theValue(energyLevel) should] equal:theValue(42.0f)]; [Cruiser stub:@selector(classification) andReturn:@"Not a moon"]; [ [ [Cruiser classification] should] equal:@"Not a moon"]; id mock = [Animal mock]; [mock stub:@selector(species) andReturn:@"P. tigris"]; [ [mock.species should] equal:@"P. tigris"];
有時,你可能想要捕捉傳遞給模擬對象的參數.比如,參數可能沒有是一個沒有很好實現 isEqual:
的對象,如果你想確認傳入的參數是否是需要的,那就要單獨根據某種自定義規則去驗證.另外一種情況,也是最長遇到的情況,就是模擬對象接收的消息的某個參數是一個block;通常必須捕捉并執行這個block才能確認這個block的行為.
示例:
id robotMock = [KWMock nullMockForClass:[YFKiwiCar class]];KWCaptureSpy *spy = [robotMock captureArgument:@selector(speak:afterDelay:whenDone:) atIndex:2]; [[robotMock should] receive:@selector(speak:) withArguments:@"Goodbye"]; [robotMock speak:@"Hello" afterDelay:2 whenDone:^{ [robotMock speak:@"Goodbye"];}]; void (^block)(void) = spy.argument;block();
未來的某天,你或許需要存根alloc
等法官法.這可能不是一個好主意,但是如果你堅持,Kiwi也是支持的.需要提前指出的是,這么做需要深入思考某些細節問題,比如如何管理初始化.
Kiwi 存根遵循 Objective-C 的內存管理機制.當存根將返回值寫入一個對象時,如果選擇器是以alloc
,或new
開頭,或含有 copy
時,retain
消息將會由存根自動在對象發送前發送.
因此,調用者不需要特別處理由存根返回的對象的內存管理問題.
Kiwi深度依賴Objective-C的運行時機制,包括消息轉發(比如 forwardInvocation:
).因為Kiwi需要預先判斷出來哪些方法可以安全調用.使用Kiwi時,有一些慣例,也是你需要遵守的.
為了使情況簡化和有條理,某些方法/選擇器,是決不能在消息模式中使用,接收期望,或者被存根;否則它們的常規行為將會被改變.不支持使用這些控制器,而且使用后的代碼的行為結果也會變的很奇怪.
在實踐中,對于高質量的程序代碼,你可能不需要擔心這些,但是最好還是對這些有些印象.
黑名單(使用有風險):
-class
, -superclass
, -retain
, -release
等.)白名單(可安全使用):
+alloc
+new
+copy
-copy
-mutableCopy
-isEqual:
-description
-hash
-init
iOS應用經常有組件需要在后臺和主線程中內容溝通.為此,Kiwi支持異步測試;因此就可以進行集成測試-一起測試多個對象.
為了設置異步測試,你 必須 使用 expectFutureValue
裝箱,并且使用 shouldEventually
或 shouldEventuallyBeforeTimingOutAfter
來驗證.
shouldEventually
默認在判定為失敗前等待一秒.
[[expectFutureValue(myObject) shouldEventually] beNonNil];
當主語中含有標量時,應該使用 expectFutureValue
中使用 theValue
裝箱標量.例如:
[[expectFutureValue(theValue(myBool)) shouldEventually] beYes];
這個block默認值是2秒而不是1秒.
[[expectFutureValue(fetchedData) shouldEventuallyBeforeTimingOutAfter(2.0)] equal:@"expected response data"];
也有shouldNotEventually
和 shouldNotEventuallyBeforeTimingOutAfter
的變體.
這個block會在匹配器滿足或者超時(默認: 1秒)時完成.
This will block until the matcher is satisfied or it times out (default: 1s)
context(@"Fetching service data", ^{ it(@"should receive data within one second", ^{ __block NSString *fetchedData = nil; [[LRResty client] get:@"http://www.example.com" withBlock:^(LRRestyResponse* r) { NSLog(@"That's it! %@", [r asString]); fetchedData = [r asString]; }]; [[expectFutureValue(fetchedData) shouldEventually] beNonNil]; }); });
新聞熱點
疑難解答