iOS单元测试之Kiwi的简介和利用

分享
手机软件开发 2024-9-13 10:23:27 77 0 来自 中国
一、Kiwi干系简介

1.1、测试驱动开发和举动驱动开发

测试驱动开发(Test Driven Development,以下简称TDD),TDD是敏捷开发中的一项核心实践和技能,也是一种计划方法论。原理呢,是在开发功能代码之前,先编写单元测试用例代码,测试代码是要根据需求的产物来编写的代码。TDD的基本思绪就是通过测试来推动整个开发的举行。测试驱动开发不是简朴的测试,是必要把需求分析、计划和质量控制量化的过程。测试驱动开发就是,在了解需求功能之后,订定了一套测试用例代码,这套测试用例代码对你的需求(对象、功能、过程、接口等)举行计划,测试框架可以连续举行验证。就像是在画画之前先画好了基本的外貌,来包管可以或许画成你想要的东西。
举动驱动开发( Behavior Driven Development,以下简称BDD), BDD是在应用步伐存在之前,写出用例与渴望,从而形貌应用步伐的举动,而且促使在项目中的人们相互相互沟通。BDD关注的是业务范畴,而不是技能。BDD强调用范畴特定语言形貌用户举动,界说业务需求,让开发者会合精力于代码的写法而不是技能细节上。偏重在整个开发层面全部到场者对举动和业务的明白。举动驱动开发将全部人会合在一起用一种特定的语言将所必要的体系举动形成一个划一明白认可的术语。就像是同一了的平常话,各个地域的人可以通过平常话来了解一句话意义是什么。
1.2、Kiwi简介

作为第二代敏捷方法,BDD提倡的是通过将测试语句转换为雷同天然语言的形貌,开发职员可以利用更符合大众语言的风俗来誊写测试,如许岂论在项目交接/交付,大概之后自己修改时,都可以顺遂很多。假如说作为开发者的我们一样平常工作是写代码,那么BDD实在就是在讲故事。一个典型的BDD的测试用例包活完备的三段式上下文,测试大多可以翻译为Given..When..Then的格式,读起来轻松惬意。BDD在其他语言中也已经有一些框架,包罗最早的Java的JBehave和赫赫闻名的Ruby的RSpec和Cucumber。而在objc社区中BDD框架也正在欣欣向荣地发展,得益于objc的语法原来就非常靠近天然语言,再加上C语言宏的威力,我们是有大概写出美丽精致的测试的。在objc中,如今比力盛行的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];        });    });});我们很容易根据上下文将其提取为Given..When..Then的三段式天然语言
Given a team, when newly created, it should have a name, and should have 11 players很简朴啊有木有!在如许的语法下,是不是写测试的爱好都被引发出来了呢。关于Kiwi的进一步语法和利用,我们稍后具体睁开。起首来看看如安在项目中添加Kiwi框架吧。
可以通过通过CocoaPods安装,请将此添加到您的Podfile:
pod "Kiwi"二、Kiwi的利用

点击下载Demo:ZJHUnitTestDemo
2.1、Kiwi测试的基本布局

可以直接创建一个平常的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的子类,并将其中的内容包装在一个函数中(有爱好的朋侪不妨点进去看看)。
describe形貌必要测试的对象内容,也即我们三段式中的Given,context形貌测试上下文,也就是这个测试在When来举行,末了it中的是测试的本体,形貌了这个测试应该满意的条件,三者共同构成了Kiwi测试中的举动形貌。它们是可以nest的,也就是一个Spec文件中可以包罗多个describe(固然我们很少这么做,一个测试文件应该专注于测试一个类);一个describe可以包罗多个context,来形貌类在差异情形下的举动;一个context可以包罗多个it的测试例。让我们运行一下这个测试,观察输出:
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规则

先看下面的第二个示例子代码
#import "Kiwi.h"#import "ZJHKiwiSample.h"// SPEC_BEGIN(ClassName) 和 SPEC_END 宏,用于标志 KWSpec 类的开始和竣事,以及测试用例的分组声明SPEC_BEGIN(ZJHKiwiSampleSpec)describe(@"ZJHKiwiSample Kiwi test", ^{    registerMatchers(@"ZJH"); // 注册全部利用"ZJH"定名空间前缀的匹配器.    context(@"a state the component is in", ^{        let(variable, ^{ // 在每个包罗的 "it" 实行前实行实行一次.            return [[ZJHKiwiSample alloc]init];        });        beforeAll(^{ // 在全部内嵌上下文或当前上下文的 it block实行之前实行一次.            NSLog(@"beforAll");        });        afterAll(^{ // 在全部内嵌上下文或当前上下文的 it block实行之后实行一次.            NSLog(@"afterAll");        });        beforeEach(^{ // 在全部包罗的上下文情况的 it block实行之前,均各实行一次.用于初始化指定上下文情况的代码            NSLog(@"beforeEach");        });        afterEach(^{ // 在全部包罗的上下文情况的 it block实行之后,均各实行一次.            NSLog(@"afterEach");        });        it(@"should do something", ^{ // 声明一个测试用例.这里形貌了对对象或举动的渴望.            NSLog(@"should do something");        });        specify(^{ // 可用于标志尚未完成的功能或用例,仅会使Xcode输出一个黄色告诫            NSLog(@"specify");            [[variable shouldNot] beNil];        });                    context(@"inner context", ^{ // 可以嵌套context            NSLog(@"inner context");            it(@"does another thing", ^{                NSLog(@"does another thing");            });            pending(@"等候实现的东西", ^{ // 可用于标志尚未完成的功能或用例,仅会使Xcode输出一个黄色告诫                NSLog(@"等候实现的东西");            });        });    });});SPEC_END

  • #import "Kiwi.h" 导入Kiwi库.这应该在规则的文件开始处开始导入.
  • SPEC_BEGIN(ClassName) 和 SPEC_END 宏,用于标志 KWSpec 类的开始和竣事,以及测试用例的分组声明.
  • registerMatchers(aNamespacePrefix) 注册全部利用指定定名空间前缀的匹配器.除了Kiwi默认的匹配器,这些匹配器也可以在当前规则中利用.
  • describe(aString, aBlock) 开启一个上下文情况,可包罗测试用例或嵌套其他的上下文情况.
  • 为了使一个block中利用的变量真正被改变,它必要在界说时利用 __block 修饰符.
  • beforeAll(aBlock) 在全部内嵌上下文或当前上下文的``it`block实行之前实行一次.
  • afterAll(aBlock) 在全部内嵌上下文或当前上下文的``it`block实行之后实行一次.
  • beforeEach(aBlock) 在全部包罗的上下文情况的 itblock实行之前,均各实行一次.用于初始化指定上下文情况的代码,应该放在这里.
  • afterEach(aBlock) 在全部包罗的上下文情况的 itblock实行之后,均各实行一次.
  • it(aString, aBlock) 声明一个测试用例.这里形貌了对对象或举动的渴望.
  • specify(aBlock) 声明一个没有形貌的测试用例.这个常用于简朴的渴望.
  • pending(aString, aBlock) 可用于标志尚未完成的功能或用例,仅会使Xcode输出一个黄色告诫.(有点TODO的赶脚)
  • let(subject, aBlock) 声明一个当地工具变量,这个变量会在规则内全部上下文的每个 itblock实行前,重新初始化一次.
2.3、渴望

渴望(Expectations),用来验证用例中的对象举动是否符合你的语气。渴望相当于传统测试中的断言,要是运行的效果不能匹配渴望,则测试失败。在Kiwi中渴望都由should大概shouldNot开头,并紧接一个或多个判定的的链式调用,大部门常见的是be大概haveSomeCondition的情势。在我们上面的例子中我们利用了should not be nil和should equal两个渴望来确保字符串赋值的举动精确。一个渴望,具有如下情势: [[subject should] someCondition:anArgument].此处 [subject should]是表达式的范例, ... someCondition:anArgument] 是匹配器的表达式。如下示例
// 可以用下面的内容更换原来的tests.m中的内容,然后cmd+u// 测试失败可自行办理;办理不了的,继续往下看.#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]]; // 应该是ZJHKiwiCar类        [[car shouldNot] conformToProtocolprotocol(NSCopying)]; // 应该没有实现NSCopying协议        [[[car should] have:4] wheels]; // 应该有4个轮子        [[theValue([(ZJHKiwiCar *)car speed]) should] equal:theValue(42.0f)]; // 测速应该是42        [[car should] receiveselector(changeToGear withArguments: theValue(3)]; // 吸收的参数应该是3        [car changeToGear: 3]; // 调用方法    });});SPEC_END2.3.1、should 和 shouldNot

[subject should] 和 [subject shouldNot] 表达式,雷同于一个吸收器,用于吸收一个渴望匹配器.他们反面紧跟的是真实的匹配表达式,这些表达式将真正被用于盘算.
默认地,主语守卫(一种机制,可以包管nil不引起瓦解)也会在[subject should ]和 [subject shouldNot]被利用时创建.给 nil 发送消息,通常不会有任何副作用.但是,你险些不会渴望:一个表达式,只是为了给某个对象通报一个无足轻重的消息,就由于对象自己是nil.也就说,向nil对象自己发送消息,并不会有任何副作用;但是在BBD里,某个要被通报消息的对象是nil,通常是非预期举动.以是,这些表达式的对象守卫机制,会将左侧无法判定为不为nil的表达式判定为 fail失败.
2.3.2、标量装箱

"装箱"是固定术语译法,实在纵然我们iOS常说的基本范例转NSObject范例(究竟如此,勿喷)。部门表达式中,匹配器表达式的参数总是NSObject对象.当将一个标量(如int整型,float浮点型等)用于必要id范例参数的地方时,应利用theValue(一个标量)宏将标量装箱.这种机制也实用于: 当一个标量必要是一个表达式的主语(主谓宾,基本语法规则,请自行脑补)时,大概一个 存根 的值必要是一个标量时.
it(@"Scalar packing",^{ // 标量装箱        [[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中,常将调用某个实例对象的方法成为给这个对象发送了某个消息.以是"消息模式"中的"消息",更多的指的的实例对象的方法;"消息模式"也就被用来判定对象的某个方法是否会调用以及是否会按照预期的方式调用。一些 Kiwi 匹配器支持利用消息模式的渴望.消息模式部门,常被放在一个表达式的后部,就像一个将要发给主语的消息一样.
it(@"Message Pattern", ^{ // 消息模式        ZJHKiwiCar *cruiser = [[ZJHKiwiCar alloc]init];        [[cruiser should] receiveselector(jumpToStarSystemWithIndex withArguments: theValue(3)];        [cruiser jumpToStarSystemWithIndex: 3]; // 渴望传的参数是3  });2.3.4、渴望:数值 和 数字

[[subject shouldNot] beNil][[subject should] beNil][[subject should] beIdenticalToid)anObject] - 比力是否完全雷同[[subject should] equalid)anObject][[subject should] equaldouble)aValue withDeltadouble)aDelta][[subject should] beWithinid)aDistance ofid)aValue][[subject should] beLessThanid)aValue][[subject should] beLessThanOrEqualToid)aValue][[subject should] beGreaterThanid)aValue][[subject should] beGreaterThanOrEqualToid)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、渴望:正则表达式匹配

[[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、渴望:数目的变革

[[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(^{ // 数目应该+1            [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(^{ // 数目应该-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]对于聚集键(即此属性/方法名对应/返回一个聚集范例的对象):[[[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];2.3.10、渴望:交互和消息

这些渴望用于验证主语是否在从创建渴望到用例竣事的这段时间里吸收到了某个消息(大概说对象的某个方法是否被调用).这个渴望会同时存储 选择器或参数等信息,并依次来决定渴望是否满意。这些渴望可用于真实或模拟的独享,但是在设置 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];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、渴望:异步调用

[[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中,自界说匹配器的最简朴方式是创建KWMatcher的子类,并以得当的方式重写下面示例中的方法.为了让你自界说的匹配器在规则中可用,你必要在规则中利用 registerMatchers(namespacePrefix)举行注册.看下Kiwi源文件中的匹配器写法(如KWEqualMatcher等),将会使你受益匪浅.
registerMatchers 待增补
2.4、模拟对象

模拟对象模拟某个类,大概依照某个写一个.他们让你在完全功能完全实现之前,就能更好地专注于对象间的交互举动,而且能低沉对象间的依赖--模拟或比克制那些运行规则时险些很难出现的情况.
it(@"Mock", ^{ // 模拟对象        id carMock = [ZJHKiwiCar mock]; // 模拟创建一个对象        [ [carMock should] beMemberOfClass:[ZJHKiwiCar class]]; // 判定对象的范例        [ [carMock should] receive:@selector(currentGear) andReturn:theValue(3)];        [ [theValue([carMock currentGear]) should] equal:theValue(3)]; // 调用模拟对象的方法        id carNullMock = [ZJHKiwiCar nullMock]; // 模拟创建一个空对象        [ [theValue([carNullMock currentGear]) should] equal:theValue(0)];        [carNullMock applyBrakes];        // 模拟协议        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 对象

通常模拟对象收到一个非预期的选择器或消息模式时,会抛出非常(PS:iOS开发常见错误奔溃之一).在模拟对象上利用 stub 或 receive渴望,渴望的消息会主动添加到模拟对象上,以实现对方法的模拟。假如你不关心模拟对象怎样处理处罚其他非预期的消息,也不想在收到非预期消息时抛出非常,那就利用 null 模拟对象吧(也即 null 对象).
当mock对象收到了没有被stub过的调用(更精确的说,走进了消息转发的forwoardInvocation:方法里)时:

  • nullMock: 就当无事发生,忽略这个调用
  • partialMock: 让初始化时传入的object来相应这个selector
  • 平常Mock:抛出exception
2.4.2、模拟类的实例

创建类的模拟实例(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]2.4.3、模拟协议的实例

创建依照某协议的实例:[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中,你可以存根真实对象(包罗类对象)或模拟对象的方法.没有指定返回值的存根,将会对应返回nil,0等零值.存根必要返回标量的,标量必要利用 theValue(某个标量)宏 装箱。全部的存根都会在规范的一个例子的末端(一个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]; // 设置对象的名字为Rolls-Royce        [mock stub:@selector(carName) andReturn:@"Rolls-Royce"];        [ [[mock carName] should] equal:@"Rolls-Royce"];                // 模拟对象吸收的消息的某个参数是一个block;通常必须捕获并实行这个block才能确认这个block的举动.        id robotMock = [KWMock nullMockForClass:[ZJHKiwiCar class]];        // 捕获block参数        KWCaptureSpy *spy = [robotMock captureArgument:@selector(speak:afterDelay:whenDone atIndex:2];        // 设置存储参数        [[robotMock should] receive:@selector(speak withArguments:@"Goodbye"];        // 模拟对象吸收的消息的某个参数是一个block        [robotMock speak:@"Hello" afterDelay:2 whenDone:^{            [robotMock speak:@"Goodbye"];        }];        // 实行block参数        void (^block)(void) = spy.argument;        block();    });2.5.1、捕获参数

偶然,你大概想要捕获通报给模拟对象的参数.比如,参数大概没有是一个没有很好实现 isEqual: 的对象,假如你想确认传入的参数是否是必要的,那就要单独根据某种自界说规则去验证.别的一种情况,也是最常遇到的情况,就是模拟对象吸收的消息的某个参数是一个block;通常必须捕获并实行这个block才能确认这个block的举动。示比方上
2.5.2、存根的内存管理标题

将来的某天,你大概必要存根alloc等方法.这大概不是一个好主意,但是假如你对峙,Kiwi也是支持的.必要提前指出的是,这么做必要深入思考某些细节标题,比如怎样管理初始化。Kiwi 存根依照 Objective-C 的内存管理机制.当存根将返回值写入一个对象时,假如选择器是以alloc,或new开头,或含有 copy时,retain消息将会由存根主动在对象发送前发送。因此,调用者不必要特别处理处罚由存根返回的对象的内存管理标题.
2.5.3、告诫

Kiwi深度依赖Objective-C的运行时机制,包罗消息转发(比如 forwardInvocation.由于Kiwi必要预先判定出来哪些方法可以安全调用.利用Kiwi时,有一些惯例,也是你必要服从的。为了使情况简化和有条理,某些方法/选择器,是决不能在消息模式中利用,吸收渴望,大概被存根;否则它们的常规举动将会被改变.不支持利用这些控制器,而且利用后的代码的举动效果也会变的很奇怪。在实践中,对于高质量的步伐代码,你大概不必要担心这些,但是最好照旧对这些有些印象
黑名单(利用有风险):

  • 全部不在白名单中的NSObject类方法和NSObject协议中的方法.(比如-class, -superclass, -retain, -release等.)
  • 全部的Kiwi对象和方法.
白名单(可安全利用):

  • +alloc
  • +new
  • +copy
  • -copy
  • -mutableCopy
  • -isEqual:
  • -description
  • -hash
  • -init
  • 其他任何不在NSObject类或NSobject协议中的方法.
2.6、异步测试

iOS应用经常有组件必要在背景和主线程中内容沟通.为此,Kiwi支持异步测试;因此就可以举行集成测试-一起测试多个对象.
2.6.1、异步测试简介

为了设置异步测试,你 必须 利用 expectFutureValue 装箱,而且利用 shouldEventually 或 shouldEventuallyBeforeTimingOutAfter来验证。shouldEventually 默认在判定为失败前等候一秒.
[[expectFutureValue(myObject) shouldEventually] beNonNil];标量的处理处罚:当主语中含有标量时,应该利用 expectFutureValue中利用 theValue装箱标量,比方:
[[expectFutureValue(theValue(myBool)) shouldEventually] beYes];shouldEventuallyBeforeTimingOutAfter():这个block默认值是2秒而不是1秒.
[[expectFutureValue(fetchedData) shouldEventuallyBeforeTimingOutAfter(2.0)] equal:@"expected response data"];也有shouldNotEventually和 shouldNotEventuallyBeforeTimingOutAfter 的变体.
2.6.2、一个示例

这个block会在匹配器满意大概超时(默认: 1秒)时完成。This will block until the matcher is satisfied or it times out (default: 1s)
   it(@"shouldEventually", ^{ // 异步测试        __block NSString *featchData = nil;                // 模拟发送请求,处理处罚异步回调        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 = @"数据返回";        });                [[expectFutureValue(featchData) shouldEventually] beNonNil];    });2.7、Kiwi利用示例

完成代码可下载:ZJHUnitTestDemo
2.7.1、测试代码(节选部门)

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;}@endPhotosViewController
static NSString * const PhotoCellIdentifier = @"hotoCell";@interface PhotosViewController () <UITableViewDataSource, UITableViewDelegate>@property (nonatomic, strong) ArrayDataSource *photosArrayDataSource;@end@implementation PhotosViewController- (void)viewDidLoad {    [super viewDidLoad];    self.navigationItem.title = @"hotos";    [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                                                         cellIdentifierhotoCellIdentifier                                                     configureCellBlock:configureCell];    self.tableView.dataSource = self.photosArrayDataSource;    [self.tableView registerClass:[PhotoCell class] forCellReuseIdentifierhotoCellIdentifier];}#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];}@end2.7.2、测试用例(节选部门)

ArrayDataSourceSpec是针对ArrayDataSource的测试用例,基本思绪是我们渴望在为一个 tableView 设置好数据源后,tableView 可以精确地从数据源获取构造 UI 所必要的信息,基本上来说,也就是可以或许得到“有多少行”以及“每行的 cell 是什么”这两个标题的答案。到这里,有写过 iOS 的开发者应该都明白我们要测试的是什么了。没错,就是 -tableView:numberOfRowsInSection: 以及 -tableView:cellForRowAtIndexPath: 这两个接口的实现。我们要测试的是 ArrayDataSource 类,因此我们天生一个实例对象。在测试中我们不渴望测试依赖于 UITableView,因此我们 mock 了一个对象取代之。接下来向 dataSource 发送扣问元素个数的方法,这里应该毫无疑问返回数组中的元素数目。接下来我们给 mockTableView 设定了一个渴望,当将向这个 mock 的 tableView 请求 dequeu indexPath 为 (0,0) 的 cell 时,将直接返回我们预先天生的一个 cell,并举行接下来的处理处罚。完成设定后,我们调用要测试的方法 [dataSource tableView:mockTableView cellForRowAtIndexPath:indexPath]。dataSource 在接到这个方法后,向 mockTableView 请求一个 cell(这个方法已经被 mock),接下来通过之前界说的 block 来对 cell 举行设置,末了返回并赋值给 result。于是,我们就得到了一个可以举行渴望断言的 result,它应该和我们之前做的 cell 是同一个对象,而且颠末了精确的设置。至此这个 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"];        };        // 天生数据源        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设置存根            [[mockTableView should] receive:@selector(dequeueReusableCellWithIdentifier:forIndexPath                                  andReturn:cell                              withArguments:@"foo",indexPath];            // dataSource 调用署理方法            result = [dataSource tableView:mockTableView cellForRowAtIndexPath:indexPath];        });                it(@"should return the dummy cell", ^{            [[result should] equal:cell];        });    });        // 获取数据方法校验    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_ENDPhotosViewControllerSpec是针对PhotosViewController的测试用例。我们模拟了 tableView 中对一个 cell 的点击,然后查抄 navigationController 的 push 操纵是否确实被调用,以及被 push 的对象是否是我们想要的下一个 ViewController。要测试的是 PhotosViewController 的实例,因此我们天生一个。对于它的 UINavigationController,由于其没有在导航栈中,也这不是我们要测试的对象(保持测试的单一性),以是用一个 mock 对象来取代。然后为其设定 -pushViewController:animated: 必要被调用的渴望。然后再用输入参数捕获将被 push 的对象抓出来,举行判定。在这里我们用 stub 更换了 photosViewController 的 navigationController,这个更换进去的 UINavigationController 的 mock 被渴望相应 -pushViewController:animated:。于是在点击 tableView 的 cell 时,我们渴望 push 一个新的 PhotoViewController 实例,这一点可以通过捕获 push 消息的参数来告竣。关于 mock 另有一点必要增补的是,利用 +mock 方法天生的 mock 对象对于渴望收到的方法是严格判定的,就是说它能且只能相应那些你添加了渴望大概 stub 的方法。比如只为一个 mock 设定了 should receive selector(a) 如许的渴望,那么对这个 mock 发送一个消息 b 的话,将会抛出非常 (固然,假如你没有向其发送消息 a 的话,测试会失败)。假如你的 mock 还必要相应其他方法的话,可以利用 +nullMock 方法来天生一个可以继承恣意预定消息而不会抛出非常的空 mock。
describe(@"hotosViewController", ^{    context(@"when click a cell in table view", ^{        it(@"A PhotoViewController should be pushed", ^{            // 新建PhotosViewController对象            PhotosViewController *photosViewController = [[PhotosViewController alloc] init];            // 判定view的创建            UIView *view = photosViewController.view;            [[view shouldNot] beNil];                        // mock一个导航条            UINavigationController *mockNavController = [UINavigationController mock];            // 设置photosViewController存根            [photosViewController stub:@selector(navigationController) andReturn:mockNavController];            // 设置mockNavController存根            [[mockNavController should] receive:@selector(pushViewController:animated];            // 添加参数捕获            KWCaptureSpy *spy = [mockNavController captureArgument:@selector(pushViewController:animated                                                           atIndex:0];            // 调用参数            [photosViewController tableView:photosViewController.tableView                    didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];                        // 获取捕获的参数            id obj = spy.argument;            PhotoViewController *vc = obj;            // 校验参数是否精确            [[vc should] beKindOfClass:[PhotoViewController class]];            [[vc.photo shouldNot] beNil];        });    });});SPEC_END三、Kiwi原理分析

3.1、构建Spec Tree

以章节 2.1、Kiwi测试的基本布局  示例为例,最开头的SPEC_BEGIN(SimpleStringSpec)和末端的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方法以返回全部测试用例对应的Invocation。在实行这个方法的过程中,会利用KWExampleSuiteBuilder构建Spec树。KWExampleSuiteBuilder会先创建一个根节点,然后调用我们的buildExampleGroups方法,以DFS的方式构建Spec树。当前的结点路径记录在KWExampleSuiteBuilder单例的contextNodeStack中,栈顶元素就是此时的context结点。
在每个结点里,都有一个KWCallSite的字段,内里有两个属性:fileName和lineNumber,用于在测试失败时准确指出标题出如今哪一行,这很紧张。这些信息是在运行时通过atos命令获取的。假如你感爱好,可以在 KWSymbolicator.m 中看到具体的实现
如许就很容易明白我们写的Spec本质上是什么了:context(...)是调用一个叫context的C函数,将当前context结点入栈,并加到上层context的子节点列表中,然后调用block()。let(...)宏睁开后是声明一个变量,并调用let_函数将一个let结点加到当前context的letNodes列表里。其他节点的举动也都大抵雷同。这里特别阐明一下it和pending,除了把自己添加到当前的context里之外,还会创建一个KWExample,后者是一个用例的抽象。它会被加到一个列表中,用于后续实行测试时调用。
在buildExampleGroups方法中,Kiwi构建了内部的Spec树,根节点记录在KWExampleSuite对象里,后者被存储在KWExampleSuiteBuilder的一个数组中。别的,在构建过程中遇到的全部it结点和pending结点,也都各自天生了KWExample对象,按照精确的次序到场到了KWExampleSuite对象中。万事俱备。如今只必要返回全部test case对应的Invocation,反面就交给体系框架去调用啦。
这些invocation的IMP是KWSpec对象里的runExample方法。但Kiwi为了给方法一个更故意义的名字,在运行时创建了新的selector,这个新selector根据当前Spec以及context的description,用驼峰定名组合而成的。固然此举是出于进步可读性的思量,但实际上组合出来的名字总是非常冗长,读起来很困难。
3.2、实行测试用例

就在刚刚,Kiwi已经构建出了一个清楚美丽的Spec Tree,并把全部效例抽象成一个个KWExample,在testInvocations方法中返回了它们对应的Invocation。如今统统已经预备妥当,体系组件要开始调用Kiwi返回的Invocation了。之前我们说了,这些Invocation的实现是runExample,它会做什么呢?
我们只讨论it结点。由于pending结点实际上并不会做什么实质性的事变。颠末层层调用,起首会进入KWExample的visitItNode:方法里。这个方法将以下全部操纵包装进一个block里(我们叫它block1):

  • 实行你写在it block里的代码——你的部门用例在这一步就已经完成了查抄
  • 对自身的verifiers举行自检——这就是查抄你另一部门用例是否通过的时机。反面我们还会具体阐明
  • 假如有expectation没有被满意,陈诉用例失败,否则陈诉通过
  • 扫除全部的spy和stub (不影响mock对象)。 这意味着假如你渴望在整个用例里都实行某个stub或spy,那么你最好把它写进beforeEach里
3.3、Mock & Stub

Mock
我们来先容一下Kiwi中天生一个Mock的方法:

  • 利用Kiwi为NSObject添加的类方法+ (id)mock; 来mock某个类
  • 利用[KWMock mockForProtocol:] 来天生一个依照了某协议的对象
  • 利用[KWMock partialMockForObject:] 来根据已有object天生一个mock了该object范例的对象
KWMock还提供了nullMockFor...方法。与上面方法的差异在于:当mock对象收到了没有被stub过的调用(更精确的说,走进了消息转发的forwoardInvocation:方法里)时:

  • nullMock: 就当无事发生,忽略这个调用
  • partialMock: 让初始化时传入的object来相应这个selector
  • 平常Mock:抛出exception
如今假设我们以[ZJHNetworkTool mock]方法天生了一个KWMock对象,来看看这个有效的功能是怎么实现的
Stub a Method
下面先容了你在stub一个mock对象时时,大概会用到的参数:

  • (SEL)selector 被stub方法的selector
  • (id (^)(NSArray *params))block* 当被stub的方法被调用时,实行这个block,此block的返回值也将作为这次调用的返回值
  • (id)firstArgument, ... argument filter, 假如在调用某个方法时,传入的参数反面argumentList中的值逐一对应且完全相称,那么这次调用就不会走stub逻辑
  • (id)returnValue 调用被stub方法时,直接返回这个值。留意:假如你渴望返回的是一个数值范例,那么你应该用theValue()函数包装它,而不是用@()指令。(theValue(0.8)√ / @(0.8)×)
当你调用了[networkMock stub:@selector(requestUrl:param:completion:) withBlock:^id(NSArray *params){..}];
KWMock将会:

  • 根据传入的selector天生一个KWMessagePattern,后者是KWStub中用于唯一区分方法的数据布局(而不是用selector)
  • 用这个KWMessagePattern天生一个KWStub对象。假如你在初始化KWMock时指定了block、returnValue、argument filter等信息,也会一并传给KWStub
  • 把KWStub他放到自身的列表里
如今你已经乐成stub了一个mock对象中的方法。如今你调用 [networkMock requestUrl:@"someURL" param:@{} completion:^(NSDictionary *respondDic) { }]时,由于KWMock对象自己没有实现这个方法,将不会真正的走到HYNetworkEngine的下载逻辑里,而是实行所谓完全消息转发。KWMock重写了那两个方法。其中:

  • - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector返回自己mock的Class或Protocol对此selector的methodSignature。假如找不到,就用默认的"v@:"构造一个返回(还认识它吧?)
  • 接下来进入了 - (void)forwardInvocation:(NSInvocation *)anInvocation方法:

    • 假如没有stub能匹配这个调用,则根据partialMock或nullMock作出差异反应
    • 假如既不是partialMock也不是nullMock,那么就看是否在自己的expectedMessagePattern列表里。这个列表包罗了被stub方法以及KWMock从NSObject中继续的白名单方法方法,如description、hash等。别的,你也可以调用Kiwi的expect...接口向这里添加messagePattern
    • 假如消息还没有被处理处罚,则抛出非常
    • 之后,KWMock将遍历自己的stub列表,让stub行止置处罚这个调用。KWStub起首会用本次invocation与自己的messagePattern举行匹配,假如匹配效果乐成,则调用你提供的block(假如有的话。留意,由于参数是用NSArray传过去的,以是全部的nil都被更换为了[NSNull null])。然后将返回值写进invocation。末了返回YES,竣事责任链
    • 起首,它会查抄是否有人(spy)渴望监听到这次调用。假如有,就关照给他

消息转发处理处罚的代码如下,至此,我们向mock对象创建和调用stub方法的步调都已经完成了
- (void)forwardInvocation:(NSInvocation *)anInvocation {    // 将本次调用关照给关心它的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

当我们写下should、shouldEventually、beNil、graterThan、receive等语句时,Kiwi为我们做了什么?延时判定是怎么实现的?前面说的registerMatchers语句有什么用?接下来我们会逐一分析。
Kiwi中对Expectation的明白是:一个对象(称它为 subject)在如今或将来的某个时间 应该(should)不应该(shouldNot) 满意某个条件。
在Kiwi中,有一个概念叫Verifier,顾名思义,是用于判定 subject 是否满意某个条件的。Verifier在Kiwi中共分为三种,分别是:

  • ExistVerifier 用于判定 subject 是否为空。相应的接口已经废弃,这里只提一下,不再分析。对应的调用方式包罗:[subject shouBeNil]
  • MatchVerifier 用于判定 subject 是否满意某个条件。对应的调用方式包罗:[[subject should] beNil]
  • AsyncVerifier MatcherVerifier的子类。差异的是,它用来实行延时判定。对应的调用方式包罗 假如你在用AsyncVerifier,别忘了用expectFutureValue函数包装你的 subject,以便在它的值改变时,Kiwi依然可以或许找到它。[[expectFutureValue(subject) shouldEventuallyBeforeTimingOutAfter(0.5)] beNil]、[[expectFutureValue(subject) shouldAfterWaitOf(0.5)] beNil]
MatchVerifier
假设我们有如许的一个Expectation:
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-19 06:20, Processed in 0.150581 second(s), 32 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表