一、单元测试
1.1 单元测试的必要性
测试驱动开发并不是一个很陈腐的概念了。在日常开发中,很多时候须要测试,然而这种输入是必须在点击一系列按钮之后能力在屏幕上显示进去的货色。测试的时候,往往是用模拟器一次一次的从头开始启动 app,而后定位到本人所在模块的程序,做一系列的点击操作,而后查看后果是否合乎本人预期。
这种行为无疑是对工夫的微小节约。于是有很多资深工程师们发现,咱们是能够在代码中结构一个相似的场景,而后在代码中调用咱们之前想要查看的代码,并将运行后果和构想后果在程序中进行比拟,如果统一,则阐明咱们的代码没有问题,由此就产生了单元测试。
1.2 单元测试的目标
单元测试的次要目标是发现模块外部逻辑、语法、算法和性能谬误。
单元测试次要是基于白盒测试验证以下问题:
- 验证代码与设计相符度。
- 发现设计和需要中存在谬误。
- 发现在编码过程中引入的谬误。
单元测试关注的重点有以下局部:
[]()
独立门路 -对于根本执行门路和循环进行测试,可能的谬误有:
- 不同数据类型的比拟。
- “差 1 错”,即可能多循环或少循环一次。
- 谬误或不可能的终止条件。
- 不适当的批改了循环变量。
部分数据结构 -单元的部分数据结构是最常见的谬误起源,应设计测试用例以查看可能的谬误:
- 不统一的数据类型。
- 查看不正确或不统一的数据类型。
错误处理 -比较完善的单元设计要能预感出错的条件,并设置适当的错误处理,以便在程序出错时,能对谬误从新做安顿,保证期逻辑上的正确性:
- 出错的形容难以了解。
- 显示的谬误与理论的谬误不符。
- 对谬误条件的解决不正确。
边界条件 -边界上呈现谬误是最常见的谬误景象:
- 取最大最小值产生谬误。
- 控制流中的大于、小于这些比拟值常呈现谬误。
单元接口 -接口实际上就是输出和输入对应关系的汇合,要对单元进行动静测试无非就是给这个单元一个输出,而后查看输入是否和预期统一。如果数据不能失常输出和输入,单元测试就无从谈起,因而须要对单元接口进行如下的测试:
- 被测单元的输出、输入在个数、属性、程序是否和具体设计中的形容统一。
- 是否批改了只做输出用的形式参数。
- 约束条件是否通过形式参数来传送。
–
1.3 单元测试依赖的两个次要框架
OCUnit(即用 XCTest 进行测试)其实就是苹果自带的测试框架,次要是断言应用,因为应用简略本次文章不过多介绍。
OCMock 次要性能是模仿某个办法或者属性的返回值,你可能会纳闷为什么要这样做? 应用模型生成的模型对象,再传进去不就能够了?答案是能够的,然而有非凡的状况,比方一些不容易结构或不容易获取的对象,此时你能够创立一个虚构的对象来实现测试。实现思维是依据要 mock 的对象的 class 来创立一个对应的对象,并且设置好该对象的属性和调用预约办法后的动作(例如返回一个值,调用代码块,发送音讯等等),而后将其记录到一个数组中,接下来开发者被动调用该办法,最初做一个 verify(验证),从而判断该办法是否被调用,或者调用过程中是否抛出异样等。在单元测试开发中应用更多难点的也是对 OCMock 的应用形式不明确,本次文章次要讲的就是这个 OCMock 的集成和应用办法。
二、OCMock 的集成与应用
2.1 OCMock 的集成形式
我的项目集成 OCMock 第三方库,这个应用 pod 工具间接装置 OCMock 框架即可。若应用 iBiu 工具装置 OCMock 库需在 podfile 文件同级创立 Podfile.custom。
[]()
应用一般的 pod 文件雷同格局增加 OCmock 如下:
source 'https://github.com/CocoaPods/Specs.git'
pod 'OCMock'
2.2 OCMock 的应用办法
(一)置换办法(存根):通知 mock 对象, 当 someMethod 被调用, 返回什么值
调用形式:
d jalopy = [OCMock mockForClass[Car class]];
OCMStub([jalopy goFaster:[OCMArg any] units:@"kph"]).andReturn(@"75kph");
应用场景:
1. 验证 A 办法时,A 办法外部应用 B 办法的返回值然而 B 办法外部逻辑比较复杂,这时须要应用 stub 办法去存根 B 办法的返回值。代码实现相似上面代码实现固定 funcB 的返回值,做到在不影响源代码的条件下,获取满足测试须要的参数。
办法进行存根前
- (NSString *)getOtherTimeStrWithString:(NSString *)formatTime{NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateStyle:NSDateFormatterMediumStyle];
[formatter setTimeStyle:NSDateFormatterShortStyle];
[formatter setDateFormat:@"YYYY-MM-dd HH:mm:ss"]; //(@"YYYY-MM-dd hh:mm:ss") ---------- 设置你想要的格局,hh 与 HH 的区别: 别离示意 12 小时制,24 小时制
// 设置时区抉择北京工夫
NSTimeZone* timeZone = [NSTimeZone timeZoneWithName:@"Asia/Beijing"];
[formatter setTimeZone:timeZone];
NSDate* date = [formatter dateFromString:formatTime]; //------------ 将字符串按 formatter 转成 nsdate
// 工夫转工夫戳的办法:
NSInteger timeSp = [[NSNumber numberWithDouble:[date timeIntervalSince1970]] integerValue] * 1000;
return [NSString stringWithFormat:@"%ld",(long)timeSp];
}
应用 stub(mockObject getOtherTimeStrWithString).andReturn(@”1000″)存根后相似于以下成果
- (NSString *)getOtherTimeStrWithString:(NSString *)formatTime{
return @"1000";
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateStyle:NSDateFormatterMediumStyle];
[formatter setTimeStyle:NSDateFormatterShortStyle];
[formatter setDateFormat:@"YYYY-MM-dd HH:mm:ss"]; //(@"YYYY-MM-dd hh:mm:ss") ---------- 设置你想要的格局,hh 与 HH 的区别: 别离示意 12 小时制,24 小时制
// 设置时区抉择北京工夫
NSTimeZone* timeZone = [NSTimeZone timeZoneWithName:@"Asia/Beijing"];
[formatter setTimeZone:timeZone];
NSDate* date = [formatter dateFromString:formatTime]; //------------ 将字符串按 formatter 转成 nsdate
// 工夫转工夫戳的办法:
NSInteger timeSp = [[NSNumber numberWithDouble:[date timeIntervalSince1970]] integerValue] * 1000;
return [NSString stringWithFormat:@"%ld",(long)timeSp];
}
2. 代码失常流程通过测试曾经很强壮了,然而一些谬误的流程并不容易发现然而是可能存在的,例如边缘值数据,单元测试中能够应用存根对数据进行模仿,测试代码在非凡数据状况下的运行状况。
注:stub()也能够不设置返回值,验证可行,猜想可能是返回的 nil 或者 void,所以不带返回值的办法也能够进行办法存根。
(二)生成 Mock 对象,目前有三种形式。
通过对 Person 类的 talk 办法进行测试举例,其中也波及 Men 类以及 Animaiton 类,以下是三个类的相干源码。
Person 类
@interface Person()
@property(nonatomic,strong)Men *men;
@end
@implementation Person
-(void)talk:(NSString *)str
{[self.men logstr:str];
[Animaiton logstr:str];
}
@end
Men 类
@implementation Men
-(NSString *)logstr:(NSString *)str
{NSLog(@"%@",str);
return str;
}
@end
Animaiton 类
@implementation Animaiton
+(NSString *)logstr:(NSString *)str
{NSLog(@"%@",str);
return str;
}
-(NSString *)logstr:(NSString *)str
{NSLog(@"%@",str);
return str;
}
@end
对 talk 办法进行单测时须要对 person 类进行 mock,以下是通过三种不同的形式生成 mock 对象,对三种形式的调用办法,应用场景都做了介绍,最初对每种形式的优缺点也做了一个表格不便区别。
Nice Mock
NiceMock 创立的 mock 对象在进行办法测试时会优先调用实例办法,若未找到实例办法,会持续调用同名的类办法。因而该办法能够用来生成 mock 对象去测试类办法也能够测试对象办法。
应用形式:
- (void)testTalkNiceMock {id mockA = OCMClassMock([Men class]);
Person *person1 = [Person new];
person1.men = mockA;
[person1 talk:@"123"];
OCMVerify([mockA logstr:[OCMArg any]]);
}
应用场景:
Nice mock 是比拟敌对的,当一个没有存根的办法被调用时他不会引起一个异样会验证通过。如果你不想本人对很多的办法进行存根,那么应用 nice mock。在上方的举例中 mockA 调用 testTalkNiceMock 时,Men 类中的 +(NSString )logstr:(NSString )str 不会执行打印操作。在调用过程中因为同时存在同名的 logstr:类办法和实例办法,会优先调用实例办法。
Strict Mock
应用形式:
测试 case 如下,mockA 是 Strict Mock 生成要调用 testTalkStrictMock 办法,则 Mock 生成要调用 testTalkStrictMock 办法则该办法要应用 stub 进行存根, 否则最初的 OCMVerifyAll(mockA)就会抛出异样。
- (void)testTalkStrictMock {id mockA = OCMStrictClassMock([Person class]);
OCMStub([mockA talk:@"123"]);
[mockA talk:@"123"];
OCMVerifyAll(mockA);
}
应用场景:
这种形式创立的 mock 对象,如果调用未 stub(stub 代表存根)的办法,会抛出一个异样。这须要保障在 mock 的生命周期中每一个独立调用的办法都是被存根的,这种办法应用比拟严格,很少应用。
Partial Mock
这样创立的对象在调用办法时: 如果办法被 stub, 调用 stub 后的办法,如果办法没有被 stub, 调用原来的对象的办法,该办法有限度只能 mock 实例对象。
应用形式:
- (void)testTalkPartialMock {id mockA = OCMPartialMock([Men new]);
Person *person1 = [Person new];
person1.men = mockA;
[person1 talk:@"123"];
OCMVerify([mockA logstr:[OCMArg any]]);
}
应用场景:
当调用一个没有被存根的办法时,会调用理论对象的该办法。当不能很好的存根一个类的办法时,该技术是十分有用的。调用 testTalkPartialMock 时 Men 类中的 +(NSString )logstr:(NSString )str 会执行打印操作。
三种形式的差别表格:
[]()
(三)验证办法的调用
调用形式:
OCMVerify([mock someMethod]);
OCMVerify(never(), [mock doStuff]); // 从没被调用
OCMVerify(times(n), [mock doStuff]); // 调用了 N 次
OCMVerify(atLeast(n), [mock doStuff]); // 起码被调用了 N 次
OCMVerify(atMost(n), [mock doStuff]);
应用场景:
在单元测试中能够验证某个办法是否执行,以及执行了几次。
延时验证调用:
OCMVerifyAllWithDelay(mock, aDelay);
应用场景:该性能用于期待异步操作会比拟多,其中 aDelay 为预期最长等待时间。
(四)增加预期
调用形式:
筹备数据:
NSDictionary *info = @{@"name": @"momo"};
id mock = OCMClassMock([MOOCMockDemo class]);
增加预期:
OCMExpect([mock handleLoadSuccessWithPerson:[OCMArg any]]);
能够预期不执行:
OCMReject([mock handleLoadFailWithPerson:[OCMArg any]]);
能够验证参数:
// 预期 + 参数验证
OCMExpect([mock handleLoadSuccessWithPerson:[OCMArg checkWithBlock:^BOOL(id obj) {MOPerson *person = (MOPerson *)obj;
return [person.name isEqualToString:@"momo"];
}]]);
能够预期执行程序:
// 预期下列办法程序执行
[mock setExpectationOrderMatters:YES];
OCMExpect([mock handleLoadSuccessWithPerson:[OCMArg any]]);
OCMExpect([mock showError:NO]);
能够疏忽参数(预期办法执行时):
OCMExpect([mock showError:YES]).ignoringNonObjectArgs; // 漠视参数
执行:
[MOOCMockDemo handleLoadFinished:info];
断言:
OCMVerifyAll(mock);
能够提早断言:
OCMVerifyAllWithDelay(mock, 1); // 反对提早验证
最初的 OCMVerifyAll 会验证后面的冀望是否无效,只有有一个没调用,就会出错。
(五)参数束缚
调用形式:
OCMStub([mock someMethodWithAnArgument:[OCMArg any]])
OCMStub([mock someMethodWithPointerArgument:[OCMArg anyPointer]])
OCMStub([mock someMethodWithSelectorArgument:[OCMArg anySelector]])
应用场景:在应用 OCMVerify()办法验证某个办法是否调用是应用, 单元测试会验证办法参数是否统一,如果不统一就是提醒验证失败,此时如果只关注办法调用,并不关注参数即可应用 [OCMArg any] 传参。
(六)网络接口的模仿
顾名思义能够 mock 网络接口的数据返回,测试不同数据下代码的走向以及准确性。
调用形式:
id mockManager = OCMClassMock([JDStoreNetwork class]);
[orderListVc setComponentsNet:mockManager];
[OCMStub([mockManager startWithSetup:[OCMArg any] didFinish:[OCMArg any] didCancel:[OCMArg any]]) andDo:^(NSInvocation *invocation) {void (^successBlock)(id components,NSError *error) = nil;
[invocation getArgument:&successBlock atIndex:3];
successBlock(@{@"code":@"1",@"resultCode":@"1",@"value":@{@"showOrderSearch":@"NO"}},nil);
}];
以上就是在调用 setComponentsNet 办法外部调用了接口,该办法就能够在调用接口后模仿须要的返回数据,successBlock 中的就是返回的测试数据。本形式是通过获取接口调用的办法签名,获取 successBlock 胜利回调传参并手动调用。同样能够模仿接口失败的状况,只需获取到签名中的对应的失败回调就能够实现了。
应用场景:书写单元测试办法时波及网络接口的模仿,通过该形式 mock 接口返回后果。
(七)复原类
置换类办法后, 能够将类复原到原来的状态, 通过调用 stopMocking 来实现。
调用形式:
id classMock = OCMClassMock([SomeClass class]);
/* do stuff */
[classMock stopMocking];
应用场景:
失常对实例对象置换后,mock 对象开释后会主动调用 stopMocking,然而增加到类办法上的 mock 对象会逾越了多个测试,mock 的类对象在置换后不会 deallocated, 须要手动来勾销这个 mock 关系。
(八)观察者模仿 - 创立一个承受告诉的实例
调用形式:
- (void)testPostNotification {Person *person1 = [[Person alloc] init];
id observerMock = OCMObserverMock();
// 给告诉核心设置观察者
[[NSNotificationCenter defaultCenter] addMockObserver: observerMock name:@"name" object:nil];
// 设置察看冀望
[[observerMock expect] notificationWithName:@"name" object:[OCMArg any]]; // 调用要验证的办法
[person1 methodWithPostNotification];
[[NSNotificationCenter defaultCenter] removeObserver:observerMock];
// 调用验证
OCMVerifyAll(observerMock);}
应用场景:
创立一个 mock 对象, 能够用来察看告诉。mock 必须注册以接管告诉。
(九)mock 协定
调用形式:
id protocolMock = OCMProtocolMock(@protocol(SomeProtocol));
/* 严格的协定 */
id classMock = OCMStrictClassMock([SomeClass class]);
id protocolMock = OCMStrictProtocolMock(@protocol(SomeProtocol));
id protocolMock = OCMProtocolMock(@protocol(SomeProtocol));
/* 严格的协定 */
id classMock = OCMStrictClassMock([SomeClass class]);
id protocolMock = OCMStrictProtocolMock(@protocol(SomeProtocol));
调用场景:当须要创立一个实例,让其具备协定的所定义的性能时应用。
2.3 mock 应用限度
对于同个办法,先 stub 后 expect 是不行的:因为先 stub 的话,所有的调用都会变成 stub,这样子即便过程调用该办法,最初 OCMVerifyAll 验证也会失败;解决的方法是,在 OCMExpect 上顺便 stub,比方:OCMExpect([mock someMethod]).andReturn(@”a string”),或者将 stub 置于 expect 之后。
局部模仿不适用于某些类:如 NSString 和 NSDate,这些”toll-free bridged”的类,否则会抛出异样。
某些办法不能 stub:如:init、class、methodSignatureForSelector、forwardInvocation 这些。
NSString 与 NSArray 的类办法不能 stub,否则有效。
NSObject 的办法调用不能验证,除非在子类中重写。
苹果外围类的公有办法调用不能被验证,如以_结尾的办法。
延时验证办法调用不反对,临时只反对冀望 - 运行 - 验证模式的延时验证。
OCMock 不反对多线程。
三、最初
心愿这篇文章和例子曾经陈说分明了一些 OCMock 最通用的用法。OCMock 站点:http://ocmock.org/features/ 是一个最好的学习 OCMock 的中央。mock 是枯燥的然而对于一个应用程序却是必须的。如果一个办法很难用 mock 来测试,这个迹象表明你的设计须要重新考虑了。
参考链接:
OCMock 官网:https://ocmock.org/features/
OCMock3 参考:https://www.cnblogs.com/xilifeng/p/4690280.html#header-c18
iOS 测试系列:http://blog.oneinbest.com/2017/07/27/iOS%E6%B5%8B%E8%AF%95%E7…
作者:京东批发 王中文
起源:京东云开发者社区