iOS單元測試之Kiwi的簡介和使用

一嘶是、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的RSpecCucumber。而在objc社區(qū)中BDD框架也正在欣欣向榮地發(fā)展涩拙,得益于objc的語法本來就非常接近自然語言际长,再加上C語言宏的威力,我們是有可能寫出漂亮優(yōu)美的測試的吃环。在objc中,現(xiàn)在比較流行的BDD框架有cedar洋幻,spectaKiwi郁轻。本文主要介紹的是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_BEGINSPEC_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)境的 itblock執(zhí)行之前,均各執(zhí)行一次.用于初始化指定上下文環(huán)境的代碼,應(yīng)該放在這里.
  • afterEach(aBlock) 在所有包含的上下文環(huán)境的 itblock執(zhí)行之后,均各執(zhí)行一次.
  • it(aString, aBlock) 聲明一個測試用例.這里描述了對對象或行為的期望.
  • specify(aBlock) 聲明一個沒有描述的測試用例.這個常用于簡單的期望.
  • pending(aString, aBlock) 可用于標(biāo)記尚未完成的功能或用例,僅會使Xcode輸出一個黃色警告.(有點TODO的趕腳)
  • let(subject, aBlock) 聲明一個本地工具變量,這個變量會在規(guī)則內(nèi)所有上下文的每個 itblock執(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ā)常見錯誤奔潰之一).在模擬對象上使用 stubreceive期望,期望的消息會自動添加到模擬對象上,以實現(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ī)范的一個例子的末尾(一個itblock)被清除.

存根選擇器:
[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 裝箱,并且使用 shouldEventuallyshouldEventuallyBeforeTimingOutAfter來驗證崖蜜。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"];

也有shouldNotEventuallyshouldNotEventuallyBeforeTimingOutAfter 的變體.

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 的點擊,然后檢查 navigationControllerpush 操作是否確實被調(diào)用界睁,以及被 push 的對象是否是我們想要的下一個 ViewController觉增。要測試的是 PhotosViewController 的實例,因此我們生成一個翻斟。對于它的 UINavigationController逾礁,因為其沒有在導(dǎo)航棧中,也這不是我們要測試的對象(保持測試的單一性)访惜,所以用一個 mock 對象來代替嘹履。然后為其設(shè)定 -pushViewController:animated: 需要被調(diào)用的期望。然后再用輸入?yún)?shù)捕獲將被 push 的對象抓出來债热,進(jìn)行判斷砾嫉。在這里我們用 stub 替換了 photosViewControllernavigationController,這個替換進(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)前contextletNodes列表里翎苫。其他節(jié)點的行為也都大致相同权埠。這里特別說明一下itpending,除了把自己添加到當(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)入KWExamplevisitItNode:方法里婶芭。這個方法將以下所有操作包裝進(jìn)一個block里(我們叫它block1):

  • 執(zhí)行你寫在it block里的代碼——你的部分用例在這一步就已經(jīng)完成了檢查
  • 對自身的verifiers進(jìn)行自檢——這就是檢查你另一部分用例是否通過的時機东臀。后面我們還會詳細(xì)說明
  • 如果有expectation沒有被滿足,報告用例失敗犀农,否則報告通過
  • 清除所有的spystub (不影響mock對象)惰赋。 這意味著如果你希望在整個用例里都執(zhí)行某個stubspy,那么你最好把它寫進(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棉饶、beNilgraterThan镇匀、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)前Exampleverifiers 列表里踱阿,并返回這個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ù) matchershouldBeEvaluatedAtEndOfExample方法返回值见间,來決定立刻調(diào)用 matcher 中實現(xiàn)的evaluate方法來檢測測試結(jié)果聊闯,還是等到整個 Example 執(zhí)行完成后(也就是說,你在這個it節(jié)點內(nèi)寫的代碼都執(zhí)行之后米诉。還記得前面執(zhí)行測試用例那一小節(jié)提到的 verifiers 自檢步驟嗎菱蔬?)才檢查。

Kiwi內(nèi)置的 matcher 中史侣,只有KWNotificationMatcherKWReceiveMatcher是在 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

上面說過卓囚,AsyncVerifierMatchVerifier的子類瘾杭。這意味著,它也是通過 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/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末画畅,一起剝皮案震驚了整個濱河市贸毕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌夜赵,老刑警劉巖明棍,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異寇僧,居然都是意外死亡摊腋,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門嘁傀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來兴蒸,“玉大人,你說我怎么就攤上這事细办〕鹊剩” “怎么了蕾殴?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長岛啸。 經(jīng)常有香客問我钓觉,道長,這世上最難降的妖魔是什么坚踩? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任荡灾,我火速辦了婚禮,結(jié)果婚禮上瞬铸,老公的妹妹穿的比我還像新娘批幌。我一直安慰自己,他們只是感情好嗓节,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布荧缘。 她就那樣靜靜地躺著,像睡著了一般拦宣。 火紅的嫁衣襯著肌膚如雪截粗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天恢着,我揣著相機與錄音桐愉,去河邊找鬼。 笑死掰派,一個胖子當(dāng)著我的面吹牛从诲,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播靡羡,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼系洛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了略步?” 一聲冷哼從身側(cè)響起描扯,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎趟薄,沒想到半個月后绽诚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡杭煎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年恩够,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片羡铲。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡蜂桶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出也切,到底是詐尸還是另有隱情扑媚,我是刑警寧澤腰湾,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站疆股,受9級特大地震影響费坊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜押桃,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一葵萎、第九天 我趴在偏房一處隱蔽的房頂上張望导犹。 院中可真熱鬧唱凯,春花似錦、人聲如沸谎痢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽节猿。三九已至票从,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間滨嘱,已是汗流浹背峰鄙。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留太雨,地道東北人吟榴。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像囊扳,于是被迫代替她去往敵國和親吩翻。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容