一嘶是、Kiwi相關(guān)簡介
1.1、測試驅(qū)動開發(fā)和行為驅(qū)動開發(fā)
測試驅(qū)動開發(fā)(Test Driven Development蛛碌,以下簡稱TDD)聂喇,TDD是敏捷開發(fā)中的一項核心實踐和技術(shù),也是一種設(shè)計方法論。原理呢希太,是在開發(fā)功能代碼之前克饶,先編寫單元測試用例代碼,測試代碼是要根據(jù)需求的產(chǎn)品來編寫的代碼誊辉。TDD的基本思路就是通過測試來推動整個開發(fā)的進(jìn)行矾湃。測試驅(qū)動開發(fā)不是簡單的測試,是需要把需求分析堕澄、設(shè)計和質(zhì)量控制量化的過程邀跃。測試驅(qū)動開發(fā)就是,在了解需求功能之后蛙紫,制定了一套測試用例代碼拍屑,這套測試用例代碼對你的需求(對象、功能坑傅、過程僵驰、接口等)進(jìn)行設(shè)計,測試框架可以持續(xù)進(jìn)行驗證唁毒。就像是在畫畫之前先畫好了基本的輪廓蒜茴,來保證能夠畫成你想要的東西。
行為驅(qū)動開發(fā)( Behavior Driven Development浆西,以下簡稱BDD), BDD是在應(yīng)用程序存在之前矮男,寫出用例與期望,從而描述應(yīng)用程序的行為室谚,并且促使在項目中的人們彼此互相溝通毡鉴。BDD關(guān)注的是業(yè)務(wù)領(lǐng)域,而不是技術(shù)秒赤。BDD強調(diào)用領(lǐng)域特定語言描述用戶行為猪瞬,定義業(yè)務(wù)需求,讓開發(fā)者集中精力于代碼的寫法而不是技術(shù)細(xì)節(jié)上入篮。著重在整個開發(fā)層面所有參與者對行為和業(yè)務(wù)的理解陈瘦。行為驅(qū)動開發(fā)將所有人集中在一起用一種特定的語言將所需要的系統(tǒng)行為形成一個一致理解認(rèn)可的術(shù)語。就像是統(tǒng)一了的普通話潮售,各個地區(qū)的人可以通過普通話來了解一句話意義是什么痊项。
1.2、Kiwi簡介
作為第二代敏捷方法酥诽,BDD提倡的是通過將測試語句轉(zhuǎn)換為類似自然語言的描述鞍泉,開發(fā)人員可以使用更符合大眾語言的習(xí)慣來書寫測試,這樣不論在項目交接/交付肮帐,或者之后自己修改時咖驮,都可以順利很多边器。如果說作為開發(fā)者的我們?nèi)粘9ぷ魇菍懘a,那么BDD其實就是在講故事托修。一個典型的BDD的測試用例包活完整的三段式上下文忘巧,測試大多可以翻譯為Given..When..Then
的格式,讀起來輕松愜意睦刃。BDD在其他語言中也已經(jīng)有一些框架砚嘴,包括最早的Java的JBehave和赫赫有名的Ruby的RSpec和Cucumber。而在objc社區(qū)中BDD框架也正在欣欣向榮地發(fā)展涩拙,得益于objc的語法本來就非常接近自然語言际长,再加上C語言宏的威力,我們是有可能寫出漂亮優(yōu)美的測試的吃环。在objc中,現(xiàn)在比較流行的BDD框架有cedar洋幻,specta和Kiwi郁轻。本文主要介紹的是Kiwi,使用Kiwi寫出的測試看起來大概會是這個樣子的:
示例如下所示:
describe(@"Team", ^{
context(@"when newly created", ^{
it(@"has a name", ^{
id team = [Team team];
[[team.name should] equal:@"Black Hawks"];
});
it(@"has 11 players", ^{
id team = [Team team];
[[[team should] have:11] players];
});
});
});
我們很容易根據(jù)上下文將其提取為Given..When..Then
的三段式自然語言
Given a team, when newly created, it should have a name, and should have 11 players
很簡單啊有木有文留!在這樣的語法下好唯,是不是寫測試的興趣都被激發(fā)出來了呢。關(guān)于Kiwi的進(jìn)一步語法和使用燥翅,我們稍后詳細(xì)展開骑篙。首先來看看如何在項目中添加Kiwi框架吧。
可以通過通過CocoaPods安裝森书,請將此添加到您的Podfile
:
pod "Kiwi"
二靶端、Kiwi的使用
點擊下載Demo:ZJHUnitTestDemo
2.1、Kiwi測試的基本結(jié)構(gòu)
可以直接創(chuàng)建一個普通的Objective-C test case class凛膏,如:ZJHFirstKiwiTests杨名,然后再里面添加Kiwi代碼:
#import "Kiwi.h"
SPEC_BEGIN(SimpleStringSpec)
describe(@"SimpleString", ^{
context(@"when assigned to 'Hello world'", ^{
NSString *greeting = @"Hello world";
it(@"should exist", ^{
[[greeting shouldNot] beNil];
});
it(@"should equal to 'Hello world'", ^{
[[greeting should] equal:@"Hello world"];
});
});
});
SPEC_END
你可能會覺得這不是objc代碼,甚至懷疑這些語法是否能夠編譯通過猖毫。其實SPEC_BEGIN
和SPEC_END
都是宏台谍,它們定義了一個KWSpec
的子類,并將其中的內(nèi)容包裝在一個函數(shù)中(有興趣的朋友不妨點進(jìn)去看看)吁断。
describe
描述需要測試的對象內(nèi)容趁蕊,也即我們?nèi)问街械?code>Given,context
描述測試上下文仔役,也就是這個測試在When
來進(jìn)行掷伙,最后it
中的是測試的本體,描述了這個測試應(yīng)該滿足的條件又兵,三者共同構(gòu)成了Kiwi測試中的行為描述炎咖。它們是可以nest的,也就是一個Spec文件中可以包含多個describe
(雖然我們很少這么做,一個測試文件應(yīng)該專注于測試一個類)乘盼;一個describe
可以包含多個context
升熊,來描述類在不同情景下的行為;一個context
可以包含多個it
的測試?yán)裾ぁW屛覀冞\行一下這個測試级野,觀察輸出:
ZJHUnitTestDemo[14459:288758] + 'SimpleString, when assigned to 'Hello world', should exist' [PASSED]
ZJHUnitTestDemo[14459:288758] + 'SimpleString, when assigned to 'Hello world', should equal to 'Hello world'' [PASSED]
2.2、Kiwi規(guī)則
先看下面的第二個示例子代碼
#import "Kiwi.h"
#import "ZJHKiwiSample.h"
// SPEC_BEGIN(ClassName) 和 SPEC_END 宏,用于標(biāo)記 KWSpec 類的開始和結(jié)束,以及測試用例的分組聲明
SPEC_BEGIN(ZJHKiwiSampleSpec)
describe(@"ZJHKiwiSample Kiwi test", ^{
registerMatchers(@"ZJH"); // 注冊所有使用"ZJH"命名空間前綴的匹配器.
context(@"a state the component is in", ^{
let(variable, ^{ // 在每個包含的 "it" 執(zhí)行前執(zhí)行執(zhí)行一次.
return [[ZJHKiwiSample alloc]init];
});
beforeAll(^{ // 在所有內(nèi)嵌上下文或當(dāng)前上下文的 it block執(zhí)行之前執(zhí)行一次.
NSLog(@"beforAll");
});
afterAll(^{ // 在所有內(nèi)嵌上下文或當(dāng)前上下文的 it block執(zhí)行之后執(zhí)行一次.
NSLog(@"afterAll");
});
beforeEach(^{ // 在所有包含的上下文環(huán)境的 it block執(zhí)行之前,均各執(zhí)行一次.用于初始化指定上下文環(huán)境的代碼
NSLog(@"beforeEach");
});
afterEach(^{ // 在所有包含的上下文環(huán)境的 it block執(zhí)行之后,均各執(zhí)行一次.
NSLog(@"afterEach");
});
it(@"should do something", ^{ // 聲明一個測試用例.這里描述了對對象或行為的期望.
NSLog(@"should do something");
});
specify(^{ // 可用于標(biāo)記尚未完成的功能或用例,僅會使Xcode輸出一個黃色警告
NSLog(@"specify");
[[variable shouldNot] beNil];
});
context(@"inner context", ^{ // 可以嵌套context
NSLog(@"inner context");
it(@"does another thing", ^{
NSLog(@"does another thing");
});
pending(@"等待實現(xiàn)的東西", ^{ // 可用于標(biāo)記尚未完成的功能或用例,僅會使Xcode輸出一個黃色警告
NSLog(@"等待實現(xiàn)的東西");
});
});
});
});
SPEC_END
-
#import "Kiwi.h"
導(dǎo)入Kiwi庫.這應(yīng)該在規(guī)則的文件開始處最先導(dǎo)入. -
SPEC_BEGIN(ClassName)
和SPEC_END
宏,用于標(biāo)記 KWSpec 類的開始和結(jié)束,以及測試用例的分組聲明. -
registerMatchers(aNamespacePrefix)
注冊所有使用指定命名空間前綴的匹配器.除了Kiwi默認(rèn)的匹配器,這些匹配器也可以在當(dāng)前規(guī)則中使用. -
describe(aString, aBlock)
開啟一個上下文環(huán)境,可包含測試用例或嵌套其他的上下文環(huán)境. - 為了使一個block中使用的變量真正被改變,它需要在定義時使用 __block 修飾符.
-
beforeAll(aBlock)
在所有內(nèi)嵌上下文或當(dāng)前上下文的``it`block執(zhí)行之前執(zhí)行一次. -
afterAll(aBlock)
在所有內(nèi)嵌上下文或當(dāng)前上下文的``it`block執(zhí)行之后執(zhí)行一次. -
beforeEach(aBlock)
在所有包含的上下文環(huán)境的it
block執(zhí)行之前,均各執(zhí)行一次.用于初始化指定上下文環(huán)境的代碼,應(yīng)該放在這里. -
afterEach(aBlock)
在所有包含的上下文環(huán)境的it
block執(zhí)行之后,均各執(zhí)行一次. -
it(aString, aBlock)
聲明一個測試用例.這里描述了對對象或行為的期望. -
specify(aBlock)
聲明一個沒有描述的測試用例.這個常用于簡單的期望. -
pending(aString, aBlock)
可用于標(biāo)記尚未完成的功能或用例,僅會使Xcode輸出一個黃色警告.(有點TODO的趕腳) -
let(subject, aBlock)
聲明一個本地工具變量,這個變量會在規(guī)則內(nèi)所有上下文的每個it
block執(zhí)行前,重新初始化一次.
2.3粹胯、期望
期望(Expectations)蓖柔,用來驗證用例中的對象行為是否符合你的語氣。期望相當(dāng)于傳統(tǒng)測試中的斷言风纠,要是運行的結(jié)果不能匹配期望况鸣,則測試失敗。在Kiwi中期望都由should
或者shouldNot
開頭竹观,并緊接一個或多個判斷的的鏈?zhǔn)秸{(diào)用镐捧,大部分常見的是be或者h(yuǎn)aveSomeCondition的形式。在我們上面的例子中我們使用了should not be nil和should equal兩個期望來確保字符串賦值的行為正確臭增。一個期望,具有如下形式: [[subject should] someCondition:anArgument]
.此處 [subject should]
是表達(dá)式的類型, ... someCondition:anArgument]
是匹配器的表達(dá)式懂酱。如下示例
// 可以用下面的內(nèi)容替換原來的tests.m中的內(nèi)容,然后cmd+u
// 測試失敗可自行解決;解決不了的,繼續(xù)往下看.
#import "Kiwi.h"
#import "ZJHKiwiCar.h"
SPEC_BEGIN(ZJHExpectationKiwiSpec)
describe(@"YFKiwiCar Test", ^{
it(@"A Car Rule", ^{
id car = [ZJHKiwiCar new];
[[car shouldNot] beNil]; // car對象不能為nil
[[car should] beKindOfClass:[ZJHKiwiCar class]]; // 應(yīng)該是ZJHKiwiCar類
[[car shouldNot] conformToProtocol:@protocol(NSCopying)]; // 應(yīng)該沒有實現(xiàn)NSCopying協(xié)議
[[[car should] have:4] wheels]; // 應(yīng)該有4個輪子
[[theValue([(ZJHKiwiCar *)car speed]) should] equal:theValue(42.0f)]; // 測速應(yīng)該是42
[[car should] receive:@selector(changeToGear:) withArguments: theValue(3)]; // 接收的參數(shù)應(yīng)該是3
[car changeToGear: 3]; // 調(diào)用方法
});
});
SPEC_END
2.3.1、should 和 shouldNot
[subject should]
和 [subject shouldNot]
表達(dá)式,類似于一個接收器,用于接收一個期望匹配器.他們后面緊跟的是真實的匹配表達(dá)式,這些表達(dá)式將真正被用于計算.
默認(rèn)地,主語守衛(wèi)(一種機制,可以保證nil不引起崩潰)也會在[subject should ]
和 [subject shouldNot]
被使用時創(chuàng)建.給 nil
發(fā)送消息,通常不會有任何副作用.但是,你幾乎不會希望:一個表達(dá)式,只是為了給某個對象傳遞一個無足輕重的消息,就因為對象本身是nil.也就說,向nil
對象本身發(fā)送消息,并不會有任何副作用;但是在BBD里,某個要被傳遞消息的對象是nil
,通常是非預(yù)期行為.所以,這些表達(dá)式的對象守衛(wèi)機制,會將左側(cè)無法判定為不為nil
的表達(dá)式判定為 fail
失敗.
2.3.2誊抛、標(biāo)量裝箱
"裝箱"是固定術(shù)語譯法,其實即使我們iOS常說的基本類型轉(zhuǎn)NSObject類型(事實如此,勿噴)列牺。部分表達(dá)式中,匹配器表達(dá)式的參數(shù)總是NSObject對象.當(dāng)將一個標(biāo)量(如int整型,float浮點型等)用于需要id
類型參數(shù)的地方時,應(yīng)使用theValue(一個標(biāo)量)
宏將標(biāo)量裝箱.這種機制也適用于: 當(dāng)一個標(biāo)量需要是一個表達(dá)式的主語(主謂賓,基本語法規(guī)則,請自行腦補)時,或者一個 存根
的值需要是一個標(biāo)量時.
it(@"Scalar packing",^{ // 標(biāo)量裝箱
[[theValue(1 + 1) should] equal:theValue(2)];
[[theValue(YES) shouldNot] equal:theValue(NO)];
[[theValue(20u) should] beBetween:theValue(1) and:theValue(30.0)];
ZJHKiwiCar * car = [ZJHKiwiCar new];
[[theValue(car.speed) should] beGreaterThan:theValue(40.0f)];
});
2.3.3、消息模式
在iOS中,常將調(diào)用某個實例對象的方法成為給這個對象發(fā)送了某個消息.所以"消息模式"中的"消息",更多的指的的實例對象的方法;"消息模式"也就被用來判斷對象的某個方法是否會調(diào)用以及是否會按照預(yù)期的方式調(diào)用拗窃。一些 Kiwi 匹配器支持使用消息模式的期望.消息模式部分,常被放在一個表達(dá)式的后部,就像一個將要發(fā)給主語的消息一樣.
it(@"Message Pattern", ^{ // 消息模式
ZJHKiwiCar *cruiser = [[ZJHKiwiCar alloc]init];
[[cruiser should] receive:@selector(jumpToStarSystemWithIndex:) withArguments: theValue(3)];
[cruiser jumpToStarSystemWithIndex: 3]; // 期望傳的參數(shù)是3
});
2.3.4瞎领、期望:數(shù)值 和 數(shù)字
[[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]
2.3.5、期望: 子串匹配
[[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!"];
2.3.6随夸、期望:正則表達(dá)式匹配
[[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];
2.3.7默刚、期望:數(shù)量的變化
[[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", ^{ // 期望: 數(shù)量的變化
NSMutableArray * array = [NSMutableArray arrayWithCapacity: 42];
[[theBlock(^{ // 數(shù)量應(yīng)該+1
[array addObject:@"foo"];
}) should] change:^{
return (NSInteger)[array count];
} by:+1];
[[theBlock(^{ // 數(shù)量不應(yīng)該改變
[array addObject:@"bar"];
[array removeObject:@"foo"];
}) shouldNot] change:^{ return (NSInteger)[array count]; }];
[[theBlock(^{ // 數(shù)量應(yīng)該-1
[array removeObject:@"bar"];
}) should] change:^{ return (NSInteger)[array count]; } by:-1];
});
2.3.8、期望:對象測試
[[subject should] beKindOfClass:(Class)aClass]
[[subject should] beMemberOfClass:(Class)aClass]
[[subject should] conformToProtocol:(Protocol *)aProtocol]
[[subject should] respondToSelector:(SEL)aSelector]
2.3.9逃魄、期望:集合
對于集合主語(即,主語是集合類型的):
[[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]
對于集合鍵(即此屬性/方法名對應(yīng)/返回一個集合類型的對象):
[[[subject should] have:(NSUInteger)aCount] collectionKey]
[[[subject should] haveAtLeast:(NSUInteger)aCount] collectionKey]
[[[subject should] haveAtMost:(NSUInteger)aCount] collectionKey]
如果主語是一個集合(比如 NSArray數(shù)組), coollectionKey 可以是任何東西(比如 items),只要遵循語法結(jié)構(gòu)就行.否則, coollectionKey應(yīng)當(dāng)是一個可以發(fā)送給主語并返回集合類型數(shù)據(jù)的消息.更進(jìn)一步說: 對于集合類型的主語,coollectionKey的數(shù)量總是根據(jù)主語的集合內(nèi)的元素數(shù)量, 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];
2.3.10荤西、期望:交互和消息
這些期望用于驗證主語是否在從創(chuàng)建期望到用例結(jié)束的這段時間里接收到了某個消息(或者說對象的某個方法是否被調(diào)用).這個期望會同時存儲 選擇器或參數(shù)等信息,并依次來決定期望是否滿足。這些期望可用于真實或模擬的獨享,但是在設(shè)置 receive
表達(dá)式時,Xcode 可能會給警告(報黃).
對參數(shù)無要求的選擇器:
[[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]
含有指定參數(shù)的選擇器:
[[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() 通配符用作參數(shù).如果你只關(guān)心一個方法的部分參數(shù)的值,這回很有用:
id subject = [Robot robot];
[[subject should] receive:@selector(speak:afterDelay:whenDone:) withArguments:@"Hello world",any(),any()];
[subject speak:@"Hello world" afterDelay:3 whenDone:nil];
2.3.11伍俘、期望:通知
[[@"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];
示例:
it(@"Notification", ^{ // 期望:通知
[[@"自定義通知" should] bePosted];
NSNotification *myNotification = [NSNotification notificationWithName:@"自定義通知"
object:nil];
[[NSNotificationCenter defaultCenter] postNotification:myNotification];
});
2.3.12邪锌、期望:異步調(diào)用
[[subject shouldEventually] receive:(SEL)aSelector]
[[subject shouldEventually] receive:(SEL)aSelector withArguments:(id)firstArgument, ...]
2.3.13、期望:異常
[[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"];
2.3.14癌瘾、自定義匹配器
Kiwi中,自定義匹配器的最簡單方式是創(chuàng)建KWMatcher的子類,并以適當(dāng)?shù)姆绞街貙懴旅媸纠械姆椒?為了讓你自定義的匹配器在規(guī)則中可用,你需要在規(guī)則中使用 registerMatchers(namespacePrefix)
進(jìn)行注冊.看下Kiwi源文件中的匹配器寫法(如KWEqualMatcher等),將會使你受益匪淺.
registerMatchers 待補充
2.4觅丰、模擬對象
模擬對象模擬某個類,或者遵循某個寫一個.他們讓你在完全功能完全實現(xiàn)之前,就能更好地專注于對象間的交互行為,并且能降低對象間的依賴--模擬或比避免那些運行規(guī)則時幾乎很難出現(xiàn)的情況.
it(@"Mock", ^{ // 模擬對象
id carMock = [ZJHKiwiCar mock]; // 模擬創(chuàng)建一個對象
[ [carMock should] beMemberOfClass:[ZJHKiwiCar class]]; // 判斷對象的類型
[ [carMock should] receive:@selector(currentGear) andReturn:theValue(3)];
[ [theValue([carMock currentGear]) should] equal:theValue(3)]; // 調(diào)用模擬對象的方法
id carNullMock = [ZJHKiwiCar nullMock]; // 模擬創(chuàng)建一個空對象
[ [theValue([carNullMock currentGear]) should] equal:theValue(0)];
[carNullMock applyBrakes];
// 模擬協(xié)議
id flyerMock = [KWMock mockForProtocol:@protocol(ZJHKiwiFlyingMachine)];
[ [flyerMock should] conformToProtocol:@protocol(ZJHKiwiFlyingMachine)];
[flyerMock stub:@selector(dragCoefficient) andReturn:theValue(17.0f)];
id flyerNullMock = [KWMock nullMockForProtocol:@protocol(ZJHKiwiFlyingMachine)];
[flyerNullMock takeOff];
});
2.4.1、模擬 Null 對象
通常模擬對象收到一個非預(yù)期的選擇器或消息模式時,會拋出異常(PS:iOS開發(fā)常見錯誤奔潰之一).在模擬對象上使用 stub
或 receive
期望,期望的消息會自動添加到模擬對象上,以實現(xiàn)對方法的模擬妨退。如果你不關(guān)心模擬對象如何處理其他非預(yù)期的消息,也不想在收到非預(yù)期消息時拋出異常,那就使用 null 模擬對象吧(也即 null 對象).
當(dāng)mock對象收到了沒有被stub過的調(diào)用(更準(zhǔn)確的說妇萄,走進(jìn)了消息轉(zhuǎn)發(fā)的forwoardInvocation:
方法里)時:
- nullMock: 就當(dāng)無事發(fā)生蜕企,忽略這個調(diào)用
- partialMock: 讓初始化時傳入的object來響應(yīng)這個selector
- 普通Mock:拋出exception
2.4.2、模擬類的實例
創(chuàng)建類的模擬實例(NSObject 擴展):
[SomeClass mock]
[SomeClass mockWithName:(NSString *)aName]
[SomeClass nullMock]
[SomeClass nullMockWithName:(NSString *)aName]
創(chuàng)建類的模擬實例:
[KWMock mockForClass:(Class)aClass]
[KWMock mockWithName:(NSString *)aName forClass:(Class)aClass]
[KWMock nullMockForClass:(Class)aClass]
[KWMock nullMockWithName:(NSString *)aName forClass:(Class)aClass]
2.4.3冠句、模擬協(xié)議的實例
創(chuàng)建遵循某協(xié)議的實例:
[KWMock mockForProtocol:(Protocol *)aProtocol]
[KWMock mockWithName:(NSString *)aName forProtocol:(Protocol *)aProtocol]
[KWMock nullMockForProtocol:(Protocol *)aProtocol]
[KWMock nullMockWithName:(NSString *)aName forProtocol:(Protocol *)aProtocol]
2.5轻掩、存根
存根,能返回指定定選擇器或消息模式的封裝好的請求.Kiwi中,你可以存根真實對象(包括類對象)或模擬對象的方法.沒有指定返回值的存根,將會對應(yīng)返回nil,0等零值.存根需要返回標(biāo)量的,標(biāo)量需要使用 theValue(某個標(biāo)量)
宏 裝箱。所有的存根都會在規(guī)范的一個例子的末尾(一個it
block)被清除.
存根選擇器:
[subject stub:(SEL)aSelector]
[subject stub:(SEL)aSelector andReturn:(id)aValue]
存根消息模式:
[ [subject stub] *messagePattern*]
[ [subject stubAndReturn:(id)aValue] *messagePattern*]
示例:
it(@"stub", ^{ // 存根
id mock = [ZJHKiwiCar mock]; // 設(shè)置對象的名字為Rolls-Royce
[mock stub:@selector(carName) andReturn:@"Rolls-Royce"];
[ [[mock carName] should] equal:@"Rolls-Royce"];
// 模擬對象接收的消息的某個參數(shù)是一個block;通常必須捕捉并執(zhí)行這個block才能確認(rèn)這個block的行為.
id robotMock = [KWMock nullMockForClass:[ZJHKiwiCar class]];
// 捕捉block參數(shù)
KWCaptureSpy *spy = [robotMock captureArgument:@selector(speak:afterDelay:whenDone:) atIndex:2];
// 設(shè)置存儲參數(shù)
[[robotMock should] receive:@selector(speak:) withArguments:@"Goodbye"];
// 模擬對象接收的消息的某個參數(shù)是一個block
[robotMock speak:@"Hello" afterDelay:2 whenDone:^{
[robotMock speak:@"Goodbye"];
}];
// 執(zhí)行block參數(shù)
void (^block)(void) = spy.argument;
block();
});
2.5.1懦底、捕捉參數(shù)
有時,你可能想要捕捉傳遞給模擬對象的參數(shù).比如,參數(shù)可能沒有是一個沒有很好實現(xiàn) isEqual:
的對象,如果你想確認(rèn)傳入的參數(shù)是否是需要的,那就要單獨根據(jù)某種自定義規(guī)則去驗證.另外一種情況,也是最常遇到的情況,就是模擬對象接收的消息的某個參數(shù)是一個block;通常必須捕捉并執(zhí)行這個block才能確認(rèn)這個block的行為唇牧。示例如上
2.5.2、存根的內(nèi)存管理問題
未來的某天,你或許需要存根alloc
等方法.這可能不是一個好主意,但是如果你堅持,Kiwi也是支持的.需要提前指出的是,這么做需要深入思考某些細(xì)節(jié)問題,比如如何管理初始化聚唐。Kiwi 存根遵循 Objective-C 的內(nèi)存管理機制.當(dāng)存根將返回值寫入一個對象時,如果選擇器是以alloc
,或new
開頭,或含有 copy
時,retain
消息將會由存根自動在對象發(fā)送前發(fā)送丐重。因此,調(diào)用者不需要特別處理由存根返回的對象的內(nèi)存管理問題.
2.5.3、警告
Kiwi深度依賴Objective-C的運行時機制,包括消息轉(zhuǎn)發(fā)(比如 forwardInvocation:
).因為Kiwi需要預(yù)先判斷出來哪些方法可以安全調(diào)用.使用Kiwi時,有一些慣例,也是你需要遵守的杆查。為了使情況簡化和有條理,某些方法/選擇器,是決不能在消息模式中使用,接收期望,或者被存根;否則它們的常規(guī)行為將會被改變.不支持使用這些控制器,而且使用后的代碼的行為結(jié)果也會變的很奇怪扮惦。在實踐中,對于高質(zhì)量的程序代碼,你可能不需要擔(dān)心這些,但是最好還是對這些有些印象
黑名單(使用有風(fēng)險):
- 所有不在白名單中的NSObject類方法和NSObject協(xié)議中的方法.(比如
-class
,-superclass
,-retain
,-release
等.) - 所有的Kiwi對象和方法.
白名單(可安全使用):
+alloc
+new
+copy
-copy
-mutableCopy
-isEqual:
-description
-hash
-init
- 其他任何不在NSObject類或NSobject協(xié)議中的方法.
2.6、異步測試
iOS應(yīng)用經(jīng)常有組件需要在后臺和主線程中內(nèi)容溝通.為此,Kiwi支持異步測試;因此就可以進(jìn)行集成測試-一起測試多個對象.
2.6.1亲桦、異步測試簡介
為了設(shè)置異步測試,你 必須 使用 expectFutureValue
裝箱,并且使用 shouldEventually
或 shouldEventuallyBeforeTimingOutAfter
來驗證崖蜜。shouldEventually
默認(rèn)在判定為失敗前等待一秒.
[[expectFutureValue(myObject) shouldEventually] beNonNil];
標(biāo)量的處理
:當(dāng)主語中含有標(biāo)量時,應(yīng)該使用 expectFutureValue
中使用 theValue
裝箱標(biāo)量烙肺,例如:
[[expectFutureValue(theValue(myBool)) shouldEventually] beYes];
shouldEventuallyBeforeTimingOutAfter()
:這個block默認(rèn)值是2秒而不是1秒.
[[expectFutureValue(fetchedData) shouldEventuallyBeforeTimingOutAfter(2.0)] equal:@"expected response data"];
也有shouldNotEventually
和 shouldNotEventuallyBeforeTimingOutAfter
的變體.
2.6.2纳猪、一個示例
這個block會在匹配器滿足或者超時(默認(rèn): 1秒)時完成氧卧。This will block until the matcher is satisfied or it times out (default: 1s)
it(@"shouldEventually", ^{ // 異步測試
__block NSString *featchData = nil;
// 模擬發(fā)送請求桃笙,處理異步回調(diào)
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
featchData = @"數(shù)據(jù)返回";
});
[[expectFutureValue(featchData) shouldEventually] beNonNil];
});
2.7、Kiwi使用示例
完成代碼可下載:ZJHUnitTestDemo
2.7.1沙绝、測試代碼(節(jié)選部分)
ArrayDataSource:
typedef void (^TableViewCellConfigureBlock)(id cell, id item);
@interface ArrayDataSource : NSObject <UITableViewDataSource>
- (id)initWithItems:(NSArray *)anItems cellIdentifier:(NSString *)aCellIdentifier configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock;
- (id)itemAtIndexPath:(NSIndexPath *)indexPath;
@end
@interface ArrayDataSource ()
@property (nonatomic, strong) NSArray *items;
@property (nonatomic, copy) NSString *cellIdentifier;
@property (nonatomic, copy) TableViewCellConfigureBlock configureCellBlock;
@end
@implementation ArrayDataSource
- (id)init {
return nil;
}
- (id)initWithItems:(NSArray *)anItems cellIdentifier:(NSString *)aCellIdentifier configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock {
self = [super init];
if (self) {
self.items = anItems;
self.cellIdentifier = aCellIdentifier;
self.configureCellBlock = [aConfigureCellBlock copy];
}
return self;
}
- (id)itemAtIndexPath:(NSIndexPath *)indexPath {
return self.items[(NSUInteger) indexPath.row];
}
#pragma mark UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.items.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:self.cellIdentifier forIndexPath:indexPath];
id item = [self itemAtIndexPath:indexPath];
self.configureCellBlock(cell, item);
return cell;
}
@end
PhotosViewController
static NSString * const PhotoCellIdentifier = @"PhotoCell";
@interface PhotosViewController () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) ArrayDataSource *photosArrayDataSource;
@end
@implementation PhotosViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"Photos";
[self setupTableView];
}
- (void)setupTableView {
TableViewCellConfigureBlock configureCell = ^(PhotoCell *cell, Photo *photo) {
[cell configureForPhoto:photo];
};
Store *st =[Store sharedInstance];
NSArray *photos = [st sortedPhotos];
self.photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos
cellIdentifier:PhotoCellIdentifier
configureCellBlock:configureCell];
self.tableView.dataSource = self.photosArrayDataSource;
[self.tableView registerClass:[PhotoCell class] forCellReuseIdentifier:PhotoCellIdentifier];
}
#pragma mark UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
PhotoViewController *photoViewController = [[PhotoViewController alloc] init];
photoViewController.photo = [self.photosArrayDataSource itemAtIndexPath:indexPath];
[self.navigationController pushViewController:photoViewController animated:YES];
}
@end
2.7.2搏明、測試用例(節(jié)選部分)
ArrayDataSourceSpec
是針對ArrayDataSource
的測試用例,基本思路是我們希望在為一個 tableView 設(shè)置好數(shù)據(jù)源后闪檬,tableView 可以正確地從數(shù)據(jù)源獲取組織 UI 所需要的信息星著,基本上來說,也就是能夠得到“有多少行”以及“每行的 cell 是什么”這兩個問題的答案粗悯。到這里虚循,有寫過 iOS 的開發(fā)者應(yīng)該都明白我們要測試的是什么了。沒錯样傍,就是 -tableView:numberOfRowsInSection:
以及 -tableView:cellForRowAtIndexPath:
這兩個接口的實現(xiàn)横缔。我們要測試的是 ArrayDataSource
類,因此我們生成一個實例對象衫哥。在測試中我們不希望測試依賴于 UITableView
茎刚,因此我們 mock 了一個對象代替之。接下來向 dataSource 發(fā)送詢問元素個數(shù)的方法撤逢,這里應(yīng)該毫無疑問返回數(shù)組中的元素數(shù)量膛锭。接下來我們給 mockTableView
設(shè)定了一個期望粮坞,當(dāng)將向這個 mock 的 tableView 請求 dequeu indexPath 為 (0,0) 的 cell 時,將直接返回我們預(yù)先生成的一個 cell初狰,并進(jìn)行接下來的處理莫杈。完成設(shè)定后,我們調(diào)用要測試的方法 [dataSource tableView:mockTableView cellForRowAtIndexPath:indexPath]
跷究。dataSource
在接到這個方法后姓迅,向 mockTableView
請求一個 cell(這個方法已經(jīng)被 mock),接下來通過之前定義的 block 來對 cell 進(jìn)行配置俊马,最后返回并賦值給 result
丁存。于是,我們就得到了一個可以進(jìn)行期望斷言的 result柴我,它應(yīng)該和我們之前做的 cell 是同一個對象解寝,并且經(jīng)過了正確的配置。至此這個 dataSource 測試完畢艘儒。
describe(@"ArrayDataSource", ^{
// init方法校驗
context(@"Initializing", ^{
it(@"should not be allowed using init", ^{
[[[[ArrayDataSource alloc] init] should] beNil];
});
});
// 配置方法校驗
context(@"Configuration", ^{
__block UITableViewCell *configuredCell = nil;
__block id configuredObject = nil;
TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b){
configuredCell = a;
configuredObject = b;
[[configuredObject should] equal:@"a"];
};
// 生成數(shù)據(jù)源
ArrayDataSource *dataSource = [[ArrayDataSource alloc] initWithItems:@[@"a", @"b"]
cellIdentifier:@"foo"
configureCellBlock:block];
// mock一個tableView
id mockTableView = [UITableView mock];
UITableViewCell *cell = [[UITableViewCell alloc] init];
__block id result = nil;
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
it(@"should receive cell request", ^{
// tableView設(shè)置存根
[[mockTableView should] receive:@selector(dequeueReusableCellWithIdentifier:forIndexPath:)
andReturn:cell
withArguments:@"foo",indexPath];
// dataSource 調(diào)用代理方法
result = [dataSource tableView:mockTableView cellForRowAtIndexPath:indexPath];
});
it(@"should return the dummy cell", ^{
[[result should] equal:cell];
});
});
// 獲取數(shù)據(jù)方法校驗
context(@"number of rows", ^{
id mockTableView = [UITableView mock];
ArrayDataSource *dataSource = [[ArrayDataSource alloc] initWithItems:@[@"a", @"b"]
cellIdentifier:@"foo"
configureCellBlock:nil];
it(@"should be 2 items", ^{
NSInteger count = [dataSource tableView:mockTableView numberOfRowsInSection:0];
[[theValue(count) should] equal:theValue(2)];
});
});
});
SPEC_END
PhotosViewControllerSpec
是針對PhotosViewController
的測試用例聋伦。我們模擬了 tableView 中對一個 cell 的點擊,然后檢查 navigationController
的 push
操作是否確實被調(diào)用界睁,以及被 push
的對象是否是我們想要的下一個 ViewController觉增。要測試的是 PhotosViewController
的實例,因此我們生成一個翻斟。對于它的 UINavigationController
逾礁,因為其沒有在導(dǎo)航棧中,也這不是我們要測試的對象(保持測試的單一性)访惜,所以用一個 mock 對象來代替嘹履。然后為其設(shè)定 -pushViewController:animated:
需要被調(diào)用的期望。然后再用輸入?yún)?shù)捕獲將被 push 的對象抓出來债热,進(jìn)行判斷砾嫉。在這里我們用 stub 替換了 photosViewController
的 navigationController
,這個替換進(jìn)去的 UINavigationController
的 mock 被期望響應(yīng) -pushViewController:animated:
窒篱。于是在點擊 tableView 的 cell 時焕刮,我們期望 push 一個新的 PhotoViewController
實例,這一點可以通過捕獲 push 消息的參數(shù)來達(dá)成墙杯。關(guān)于 mock 還有一點需要補充的是配并,使用 +mock
方法生成的 mock 對象對于期望收到的方法是嚴(yán)格判定的,就是說它能且只能響應(yīng)那些你添加了期望或者 stub 的方法霍转。比如只為一個 mock 設(shè)定了 should receive selector(a)
這樣的期望桥爽,那么對這個 mock 發(fā)送一個消息 b 的話径簿,將會拋出異常 (當(dāng)然,如果你沒有向其發(fā)送消息 a 的話包蓝,測試會失敗)。如果你的 mock 還需要相應(yīng)其他方法的話,可以使用 +nullMock
方法來生成一個可以接受任意預(yù)定消息而不會拋出異常的空 mock。
describe(@"PhotosViewController", ^{
context(@"when click a cell in table view", ^{
it(@"A PhotoViewController should be pushed", ^{
// 新建PhotosViewController對象
PhotosViewController *photosViewController = [[PhotosViewController alloc] init];
// 判斷view的創(chuàng)建
UIView *view = photosViewController.view;
[[view shouldNot] beNil];
// mock一個導(dǎo)航條
UINavigationController *mockNavController = [UINavigationController mock];
// 設(shè)置photosViewController存根
[photosViewController stub:@selector(navigationController) andReturn:mockNavController];
// 設(shè)置mockNavController存根
[[mockNavController should] receive:@selector(pushViewController:animated:)];
// 添加參數(shù)捕捉
KWCaptureSpy *spy = [mockNavController captureArgument:@selector(pushViewController:animated:)
atIndex:0];
// 調(diào)用參數(shù)
[photosViewController tableView:photosViewController.tableView
didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
// 獲取捕捉的參數(shù)
id obj = spy.argument;
PhotoViewController *vc = obj;
// 校驗參數(shù)是否正確
[[vc should] beKindOfClass:[PhotoViewController class]];
[[vc.photo shouldNot] beNil];
});
});
});
SPEC_END
三、Kiwi原理分析
3.1监憎、構(gòu)建Spec Tree
以章節(jié) 2.1、Kiwi測試的基本結(jié)構(gòu)
示例為例婶溯,最開頭的SPEC_BEGIN(SimpleStringSpec)
和結(jié)尾的SPEC_END
鲸阔。這是兩個宏,我們來看看它們的定義:
// Example group declarations.
#define SPEC_BEGIN(name) \
\
@interface name : KWSpec \
\
@end \
\
@implementation name \
\
+ (NSString *)file { return @__FILE__; } \
\
+ (void)buildExampleGroups { \
[super buildExampleGroups]; \
\
id _kw_test_case_class = self; \
{ \
/* The shadow `self` must be declared inside a new scope to avoid compiler warnings. */ \
/* The receiving class object delegates unrecognized selectors to the current example. */ \
__unused name *self = _kw_test_case_class;
#define SPEC_END \
} \
} \
\
@end
通過這段定義我們知道了兩件事:
- 我們聲明的
SimpleStringSpec
類是KWSpec
的子類迄委,重寫了一個叫buildExampleGroups
的方法 - 我們的測試代碼是放在
buildExampleGroups
的方法體里的
實際上褐筛,KWSpec
作為XCTextCase
的子類,重寫了+ (NSArray *)testInvocations
方法以返回所有測試用例對應(yīng)的Invocation
叙身。在執(zhí)行這個方法的過程中渔扎,會使用KWExampleSuiteBuilder
構(gòu)建Spec
樹。KWExampleSuiteBuilder
會先創(chuàng)建一個根節(jié)點信轿,然后調(diào)用我們的buildExampleGroups
方法晃痴,以DFS的方式構(gòu)建Spec
樹。當(dāng)前的結(jié)點路徑記錄在KWExampleSuiteBuilder
單例的contextNodeStack
中财忽,棧頂元素就是此時的context
結(jié)點倘核。
在每個結(jié)點里,都有一個KWCallSite的字段即彪,里面有兩個屬性:fileName和lineNumber紧唱,用于在測試失敗時精確指出問題出現(xiàn)在哪一行,這很重要祖凫。這些信息是在運行時通過
atos
命令獲取的琼蚯。如果你感興趣酬凳,可以在 KWSymbolicator.m 中看到具體的實現(xiàn)
這樣就很容易理解我們寫的Spec
本質(zhì)上是什么了:context(...)
是調(diào)用一個叫context
的C函數(shù)惠况,將當(dāng)前context
結(jié)點入棧,并加到上層context
的子節(jié)點列表中宁仔,然后調(diào)用block()
稠屠。let(...)
宏展開后是聲明一個變量,并調(diào)用let_
函數(shù)將一個let
結(jié)點加到當(dāng)前context
的letNodes
列表里翎苫。其他節(jié)點的行為也都大致相同权埠。這里特別說明一下it
和pending
,除了把自己添加到當(dāng)前的context
里之外煎谍,還會創(chuàng)建一個KWExample
攘蔽,后者是一個用例的抽象。它會被加到一個列表中呐粘,用于后續(xù)執(zhí)行測試時調(diào)用满俗。
在buildExampleGroups
方法中转捕,Kiwi構(gòu)建了內(nèi)部的Spec
樹,根節(jié)點記錄在KWExampleSuite
對象里唆垃,后者被存儲在KWExampleSuiteBuilder
的一個數(shù)組中五芝。此外,在構(gòu)建過程中遇到的所有it
結(jié)點和pending
結(jié)點辕万,也都各自生成了KWExample
對象枢步,按照正確的順序加入到了KWExampleSuite
對象中。萬事俱備〗ツ颍現(xiàn)在只需要返回所有test case對應(yīng)的Invocation醉途,后面就交給系統(tǒng)框架去調(diào)用啦。
這些invocation的IMP是
KWSpec
對象里的runExample
方法砖茸。但Kiwi為了給方法一個更有意義的名字结蟋,在運行時創(chuàng)建了新的selector,這個新selector根據(jù)當(dāng)前Spec
以及context
的description渔彰,用駝峰命名組合而成的嵌屎。雖然此舉是出于提高可讀性的考慮,但實際上組合出來的名字總是非常冗長恍涂,讀起來很困難宝惰。
3.2、執(zhí)行測試用例
就在剛剛再沧,Kiwi已經(jīng)構(gòu)建出了一個清晰漂亮的Spec Tree
尼夺,并把所有用例抽象成一個個KWExample
,在testInvocations
方法中返回了它們對應(yīng)的Invocation〕慈常現(xiàn)在一切已經(jīng)準(zhǔn)備妥當(dāng)淤堵,系統(tǒng)組件要開始調(diào)用Kiwi返回的Invocation了。之前我們說了顷扩,這些Invocation的實現(xiàn)是runExample
拐邪,它會做什么呢?
我們只討論it
結(jié)點隘截。因為pending
結(jié)點實際上并不會做什么實質(zhì)性的事情扎阶。經(jīng)過層層調(diào)用,首先會進(jìn)入KWExample
的visitItNode:
方法里婶芭。這個方法將以下所有操作包裝進(jìn)一個block里(我們叫它block1
):
- 執(zhí)行你寫在
it
block里的代碼——你的部分用例在這一步就已經(jīng)完成了檢查 - 對自身的
verifiers
進(jìn)行自檢——這就是檢查你另一部分用例是否通過的時機东臀。后面我們還會詳細(xì)說明 - 如果有
expectation
沒有被滿足,報告用例失敗犀农,否則報告通過 -
清除所有的
spy
和stub
(不影響mock
對象)惰赋。 這意味著如果你希望在整個用例里都執(zhí)行某個stub
或spy
,那么你最好把它寫進(jìn)beforeEach
里
3.3呵哨、Mock & Stub
Mock
我們來介紹一下Kiwi中生成一個Mock的方法:
- 使用Kiwi為NSObject添加的類方法
+ (id)mock;
來mock某個類 - 使用
[KWMock mockForProtocol:]
來生成一個遵循了某協(xié)議的對象 - 使用
[KWMock partialMockForObject:]
來根據(jù)已有object生成一個mock了該object類型的對象
KWMock
還提供了nullMockFor...
方法赁濒。與上面方法的不同在于:當(dāng)mock對象收到了沒有被stub過的調(diào)用(更準(zhǔn)確的說贵扰,走進(jìn)了消息轉(zhuǎn)發(fā)的forwoardInvocation:
方法里)時:
- nullMock: 就當(dāng)無事發(fā)生,忽略這個調(diào)用
- partialMock: 讓初始化時傳入的object來響應(yīng)這個selector
- 普通Mock:拋出exception
現(xiàn)在假設(shè)我們以[ZJHNetworkTool mock]
方法生成了一個KWMock
對象流部,來看看這個有用的功能是怎么實現(xiàn)的
Stub a Method
下面介紹了你在stub一個mock對象時時戚绕,可能會用到的參數(shù):
- (SEL)selector 被stub方法的selector
- (id (^)(NSArray *params))block* 當(dāng)被stub的方法被調(diào)用時,執(zhí)行這個block枝冀,此block的返回值也將作為這次調(diào)用的返回值
- (id)firstArgument, ... argument filter, 如果在調(diào)用某個方法時舞丛,傳入的參數(shù)不和argumentList中的值一一對應(yīng)且完全相等,那么這次調(diào)用就不會走stub邏輯
-
(id)returnValue 調(diào)用被stub方法時果漾,直接返回這個值球切。注意:如果你希望返回的是一個數(shù)值類型,那么你應(yīng)該用
theValue()
函數(shù)包裝它绒障,而不是用@()
指令吨凑。(theValue(0.8)
√ /@(0.8)
×)
當(dāng)你調(diào)用了[networkMock stub:@selector(requestUrl:param:completion:) withBlock:^id(NSArray *params){..}];
KWMock將會:
- 根據(jù)傳入的selector生成一個
KWMessagePattern
,后者是KWStub
中用于唯一區(qū)分方法的數(shù)據(jù)結(jié)構(gòu)(而不是用selector) - 用這個
KWMessagePattern
生成一個KWStub
對象户辱。如果你在初始化KWMock
時指定了block鸵钝、returnValue、argument filter等信息庐镐,也會一并傳給KWStub
- 把
KWStub
他放到自身的列表里
現(xiàn)在你已經(jīng)成功stub了一個mock對象中的方法《魃蹋現(xiàn)在你調(diào)用 [networkMock requestUrl:@"someURL" param:@{} completion:^(NSDictionary *respondDic) { }]
時,由于KWMock
對象本身沒有實現(xiàn)這個方法必逆,將不會真正的走到HYNetworkEngine
的下載邏輯里怠堪,而是執(zhí)行所謂完全消息轉(zhuǎn)發(fā)。KWMock
重寫了那兩個方法名眉。其中:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
返回自己mock的Class或Protocol對此selector的methodSignature粟矿。如果找不到,就用默認(rèn)的"v@:"
構(gòu)造一個返回(還認(rèn)識它吧?)-
接下來進(jìn)入了
- (void)forwardInvocation:(NSInvocation *)anInvocation
方法:- 如果沒有stub能匹配這個調(diào)用损拢,則根據(jù)partialMock或nullMock作出不同反應(yīng)
- 如果既不是partialMock也不是nullMock陌粹,那么就看是否在自己的expectedMessagePattern列表里。這個列表包含了被stub方法以及KWMock從NSObject中繼承的白名單方法方法探橱,如description申屹、hash等绘证。此外隧膏,你也可以調(diào)用Kiwi的
expect...
接口向這里添加messagePattern - 如果消息還沒有被處理,則拋出異常
- 之后嚷那,KWMock將遍歷自己的stub列表胞枕,讓stub去處理這個調(diào)用。KWStub首先會用本次invocation與自己的messagePattern進(jìn)行匹配魏宽,如果匹配結(jié)果成功腐泻,則調(diào)用你提供的block(如果有的話决乎。注意,因為參數(shù)是用NSArray傳過去的派桩,所以所有的
nil
都被替換為了[NSNull null]
)构诚。然后將返回值寫進(jìn)invocation。最后返回YES铆惑,結(jié)束責(zé)任鏈 - 首先范嘱,它會檢查是否有人(spy)希望監(jiān)聽到這次調(diào)用。如果有员魏,就通知給他
消息轉(zhuǎn)發(fā)處理的代碼如下丑蛤,至此,我們向mock對象創(chuàng)建和調(diào)用stub方法的步驟都已經(jīng)完成了
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 將本次調(diào)用通知給關(guān)心它的spies
for (KWMessagePattern *messagePattern in self.messageSpies) {
if ([messagePattern matchesInvocation:invocation]) {
NSArray *spies = [self.messageSpies objectForKey:messagePattern];
for (id<KWMessageSpying> spy in spies) {
[spy object:self didReceiveInvocation:invocation];
}
}
}
for (KWStub *stub in self.stubs) {
if ([stub processInvocation:invocation])
return;
}
if (self.isPartialMock)
[anInvocation invokeWithTarget:self.mockedObject];
if (self.isNullMock)
return;
// expectedMessagePattern除了所有被stub的方法外
// 還包括KWMock從NSObject中繼承的白名單方法方法撕阎,如description受裹、hash等
for (KWMessagePattern *expectedMessagePattern in self.expectedMessagePatterns) {
if ([expectedMessagePattern matchesInvocation:anInvocation])
return;
}
[NSException raise:@"KWMockException" format:@"description"];
}
3.4、Verifier and Matcher
當(dāng)我們寫下should
虏束、shouldEventually
棉饶、beNil
、graterThan
镇匀、receive
等語句時砰盐,Kiwi為我們做了什么?延時判斷是怎么實現(xiàn)的坑律?前面說的registerMatchers
語句有什么用岩梳?接下來我們會一一分析。
Kiwi中對
Expectation
的理解是:一個對象(稱它為 subject)在現(xiàn)在或?qū)淼哪硞€時候 應(yīng)該(should) 或 不應(yīng)該(shouldNot) 滿足某個條件晃择。
在Kiwi中冀值,有一個概念叫Verifier
,顧名思義宫屠,是用于判斷 subject 是否滿足某個條件的列疗。Verifier
在Kiwi中共分為三種,分別是:
-
ExistVerifier 用于判斷 subject 是否為空浪蹂。相應(yīng)的接口已經(jīng)廢棄抵栈,這里只提一下,不再分析坤次。對應(yīng)的調(diào)用方式包括:
[subject shouBeNil]
-
MatchVerifier 用于判斷 subject 是否滿足某個條件古劲。對應(yīng)的調(diào)用方式包括:
[[subject should] beNil]
-
AsyncVerifier
MatcherVerifier
的子類。不同的是缰猴,它用來執(zhí)行延時判斷产艾。對應(yīng)的調(diào)用方式包括 如果你在用AsyncVerifier
,別忘了用expectFutureValue
函數(shù)包裝你的 subject,以便在它的值改變時闷堡,Kiwi依然能夠找到它隘膘。[[expectFutureValue(subject) shouldEventuallyBeforeTimingOutAfter(0.5)] beNil]
、[[expectFutureValue(subject) shouldAfterWaitOf(0.5)] beNil]
MatchVerifier
假設(shè)我們有這樣的一個Expectation
:
[[resultError should] equal:[NSNull null]];
這段代碼中杠览,should
實際上是一個宏弯菊,它創(chuàng)建了一個MatchVerifier
,把它添加到當(dāng)前Example
的 verifiers 列表里踱阿,并返回這個MatchVerifier
误续。接下來,我們調(diào)用了equal
方法扫茅。實際上蹋嵌,MatchVerifier
并沒有實現(xiàn)這個方法,因此會走進(jìn)轉(zhuǎn)發(fā)邏輯葫隙。在forwardInvocation:
方法中栽烂,MatchVerifier
會從 matcherFactory 中查找實現(xiàn)了equal
方法的Matcher
。后者是一個遵循KWMatching
協(xié)議的對象恋脚,用來判斷 subject 是否滿足某個條件腺办。matcherFactory 最終找到了一個Kiwi中內(nèi)置的,叫KWEqualMatcher
的類糟描,它實現(xiàn)了equal
方法怀喉,并且沒有在自己的canMatchSubject:
方法中返回 NO。因此船响,MatchVerifier
會將消息轉(zhuǎn)發(fā)給它的實例躬拢。
之后,MatchVerifier
會根據(jù) matcher 的shouldBeEvaluatedAtEndOfExample
方法返回值见间,來決定立刻調(diào)用 matcher 中實現(xiàn)的evaluate
方法來檢測測試結(jié)果聊闯,還是等到整個 Example 執(zhí)行完成后(也就是說,你在這個it
節(jié)點內(nèi)寫的代碼都執(zhí)行之后米诉。還記得前面執(zhí)行測試用例那一小節(jié)提到的 verifiers 自檢步驟嗎菱蔬?)才檢查。
Kiwi內(nèi)置的 matcher 中史侣,只有
KWNotificationMatcher
和KWReceiveMatcher
是在 Example 執(zhí)行完成后進(jìn)行檢查的拴泌,其余都是立即檢查
registerMatchers
現(xiàn)在我們已經(jīng)知道 matcherFactory 注冊和使用 matcher 的原理了,自定義一個 matcher 也是水到渠成的事情惊橱。事實上蚪腐,我們只需要創(chuàng)建一個遵循KWMatching
協(xié)議的類——當(dāng)然,繼承KWMatcher
或許是一個更方便的選擇李皇。這個類中需要實現(xiàn)的方法和其作用削茁,我們大部分都已經(jīng)說過了宙枷。接下來掉房,在當(dāng)前的 context 下使用registerMatchers
函數(shù)將你的 matcher 注冊給 matcherFactory茧跋,記得傳入的參數(shù)要和你剛剛創(chuàng)建的 matcher 類名前綴嚴(yán)格一致。
AsyncVerifier
上面說過卓囚,AsyncVerifier
是MatchVerifier
的子類瘾杭。這意味著,它也是通過 matcherFactory 提供的 matcher 去判斷你的 Expectation 是否通過的哪亿。唯一不同的是粥烁,它會以0.1s為周期對結(jié)果進(jìn)行輪詢。具體的實現(xiàn)方式為:在當(dāng)前線程使用 Default 模式蝇棉,以0.1s為時長運行RunLoop讨阻。這意味著,雖然它的名字帶了Async
篡殷,但實際上它的輪詢操作是同步執(zhí)行的钝吮。你最好把AsyncVerifier
這個名字理解為:用于測試你的Async
操作結(jié)果的Verifyer
。
所以板辽,一般情況下沒有必要把等待時間設(shè)置得過長奇瘦。
AsyncVerifier
有兩種使用方法,分別是shouldEventually...
和shouldAfterWait...
劲弦,你可以指定等待的時間耳标,否則默認(rèn)為1秒。兩種方法的區(qū)別在于:前者在輪詢過程中發(fā)現(xiàn)預(yù)期的結(jié)果已經(jīng)滿足邑跪,會立刻返回次坡。后者則會固定執(zhí)行到給定的等待時間結(jié)束后才檢測結(jié)果。
參考鏈接:
TDD的iOS開發(fā)初步以及Kiwi使用入門:https://onevcat.com/2014/02/ios-test-with-kiwi/
Kiwi,BDD行為測試框架--iOS攻城獅進(jìn)階必備技能:https://cloud.tencent.com/developer/article/1011286
iOS 自動化測試框架 Kiwi 的使用介紹及原理分析:https://cloud.tencent.com/developer/article/1972234
Kiwi 使用進(jìn)階 Mock, Stub, 參數(shù)捕獲和異步測試:https://onevcat.com/2014/05/kiwi-mock-stub-test/