当前位置: 首页 > 科技观察

OCMock,iOS单元测试通用框架,详解

时间:2023-03-12 10:48:42 科技观察

单元测试01单元测试的必要性测试驱动开发并不是一个很新的概念。在日常开发中,经常需要测试,但这种输出是点击一系列按钮后必须显示在屏幕上的东西。在测试的时候,你经常会用模拟器一遍又一遍的从头启动app,然后定位到你所在模块的程序,做一系列的点击操作,然后检查结果是否符合你的预期。这种行为无疑是对时间的巨大浪费。所以很多资深工程师发现,我们可以在代码中构造一个类似的场景,然后在代码中调用之前我们要检查的代码,将运行结果与程序中的预期结果进行比较。如果一致,那么说明我们的代码没有问题,单元测试就生成了。02单元测试的目的单元测试的主要目的是发现模块内部逻辑、语法、算法、功能等方面的错误。单元测试主要基于白盒测试来验证以下问题:验证代码是否与设计一致。查找设计和需求中的错误。发现编码过程中引入的错误。以下部分是单元测试的重点:独立路径——用于测试基本的执行路径和循环,可能的错误是:不同数据类型的比较。“错误1错误”,即多一个循环或少一个循环都可能。错误或不可能的终止条件。循环变量的不当修改。本地数据结构——单元格的本地数据结构是最常见的错误来源,测试用例的设计应检查可能的错误:不一致的数据类型。检查不正确或不一致的数据类型。错误处理——一个比较完善的单元设计应该能够预见错误情况,并设置适当的错误处理,以便在程序失败时,重新排列错误,保证逻辑正确:错误的描述难以理解.显示的错误与实际错误不符。错误条件的不正确处理。边界条件-边界错误是最常见的错误现象:在取最大值和最小值时发生错误。控制流中的大于和小于比较通常是错误的。单元接口——接口实际上是输入输出对应关系的集合。对一个单元进行动态测试无非是给单元一个输入,然后检查输出是否与预期一致。如果数据不能正常输入输出,单元测试就无从谈起。因此,需要对单元接口进行如下测试:被测单元的输入输出编号、属性、顺序是否与详细设计中的描述一致。是否修改仅用于输入的形参。约束是否通过形式参数传递。03单元测试依赖的两个主要框架OCUnit(即用XCTest测试),其实是苹果自己的测试框架,主要用于断言。由于使用简单,本文不做过多介绍。OCMock的主要功能是模拟某个方法或属性的返回值。您可能想知道为什么要这样做?使用模型生成的模型对象,然后传入?答案是肯定的,但也有特殊情况,比如有些对象不容易构造或获取,这时候可以创建一个dummyobject来完成测试。实现思路是根据被mock对象的类创建对应的对象,设置对象的属性和调用预定方法后的动作(如返回值、调用代码块、发送消息等)等),然后记录成一个数组,然后开发者主动调用该方法,最后进行一次verify(验证)判断该方法是否被调用,或者调用过程中是否抛出异常等。它也不清楚如何使用单元测试开发中比较难用的OCMock。本文主要讲一下这个OCMock的集成和使用。OCMock的集成以及使用01OCMock的集成方法该项目集成了OCMock第三方库。这可以通过使用pod工具直接安装OCMock框架来完成。如果使用iBiu工具安装OCMock库,需要在podfile文件同级创建Podfile.custom。使用与普通pod文件相同的格式添加OCmock如下:source'https://github.com/CocoaPods/Specs.git'pod'OCMock'02OCMock的使用方法(一)替换方法(stub):告诉mock对象,当someMethod被调用时,返回什么值Callmethod:djalopy=[OCMockmockForClass[Carclass]];OCMStub([jalopygoFaster:[OCMArgany]units:@"kph"]).andReturn(@"75kph");使用场景:1.方法A验证时,方法A内部使用方法B的返回值,但是方法B的内部逻辑比较复杂。这时候就需要使用stub方法对方法B的返回值进行stub,代码实现类似于下面的代码,实现funcB的固定返回值,这样就可以得到满足测试要求的参数在不影响源代码的情况下。存根之前的方法-(NSString*)getOtherTimeStrWithString:(NSString*)formatTime{NSDateFormatter*formatter=[[NSDateFormatteralloc]init];[格式化程序setDateStyle:NSDateFormatterMediumStyle];[格式化程序setTimeStyle:NSDateFormatterShortStyle];[格式化程序setDateFormat:@"YYYY-MM-ddHH:mm:ss"];//(@"YYYY-MM-ddhh:mm:ss")----------设置你想要的格式,hh和HH区别:分别是12小时制和24小时制//设置时区选择北京时间NSTimeZone*timeZone=[NSTimeZonetimeZoneWithName:@"Asia/Beijing"];[格式化程序设置时区:时区];NSDate*date=[格式化程序dateFromString:formatTime];//------------Convertstringtonsdateaccordingtoformatter//时间转时间戳的方法:NSIntegertimeSp=[[NSNumbernumberWithDouble:[datetimeIntervalSince1970]]integerValue]*1000;return[NSStringstringWithFormat:@"%ld",(long)timeSp];}使用stub(mockObjectgetOtherTimeStrWithString).andReturn(@"1000")存根以下效果-(NSString*)getOtherTimeStrWithString:(NSString*)formatTime{返回@“1000”;NSDateFormatter*格式化程序=[[NSDateFormatter分配]初始化];[格式化程序setDateStyle:NSDateFormatterMediumStyle];[格式化程序setTimeStyle:NSDateFormatterShortStyle];[格式化程序setDateFormat:@"YYYY-MM-ddHH:mm:ss"];//(@"YYYY-MM-ddhh:mm:ss")----------设置你想要的格式,hh和HH的区别:分别代表12小时制,24小时制system//设置时区选择北京时间NSTimeZone*timeZone=[NSTimeZonetimeZoneWithName:@"Asia/Beijing"];[格式化程序设置时区:时区];NSDate*date=[格式化程序dateFromString:formatTime];//------------按字符串Convertformattertonsdate//Timetotimestamp方法:NSIntegertimeSp=[[NSNumbernumberWithDouble:[datetimeIntervalSince1970]]integerValue]*1000;返回[NSStringstringWithFormat:@"%ld",(long)timeSp];}2。代码的正常流程已经过测试,非常健壮,但是有些错误的流程不容易发现但是可能存在,比如边值数据,单元测试中可以使用stubs来模拟数据,测试代码是在特殊数据条件下运行状态注:stub()也可以不设置返回值,验证可行。猜测可能返回nil或void,所以没有返回值的方法也可以用于方法存根。(2)目前Mock对象的生成方式有3种。通过Person类的talk方法的测试实例,其中还涉及到Men类和Animaiton类,下面是三个类的相关源码。Person类@interfacePerson()@property(nonatomic,strong)Men*men;@end@implementationPerson-(void)talk:(NSString*)str{[self.menlogstr:str];[动画logstr:str];}@endMenclass@implementationMen-(NSString*)logstr:(NSString*)str{NSLog(@"%@",str);返回str;}@endAnimaiton类@implementationAnimaiton+(NSString*)logstr:(NSString*)str{NSLog(@"%@",str);返回str;}-(NSString*)logstr:(NSString*)str{NSLog(@"%@",str);returnstr;}@endpairtalk方法在执行单个测试时需要模拟person类。以下是生成模拟对象的三种不同方法。分别介绍了三种方法的调用方式和使用场景。最后,还描述了每种方法的优点和缺点。建表方便区分。NiceMockNiceMock创建的mock对象在进行方法测试时会先调用实例方法。如果没有找到实例方法,就会继续调用类的同名方法。因此,可以使用该方法生成模拟对象来测试类方法和对象方法。用法:-(void)testTalkNiceMock{idmockA=OCMClassMock([Menclass]);Person*person1=[Personnew];person1.men=mockA;[person1talk:@"123"];OCMVerify([mockAlogstr:[OCMArgany]]);}使用场景:nicemock比较友好,当调用没有存根的方法时,不会引发异常,会通过验证。如果您不想自己存根很多方法,请使用漂亮的模拟。上面的例子中,mockA调用testTalkNiceMock时,Men类中的+(NSString*)logstr:(NSString*)str不会执行打印操作。在调用过程中,由于同时存在同名的log??str:类方法和实例方法,所以会先调用实例方法。StrictMock的使用方法:测试用例如下。MockA由StrictMock生成,调用testTalkStrictMock方法。如果生成Mock并调用testTalkStrictMock方法,该方法必须使用存根,否则最后的OCMVerifyAll(mockA)会抛出异常。-(void)testTalkStrictMock{idmockA=OCMStrictClassMock([Personclass]);OCMStub([mockAtalk:@"123"]);[模拟谈话:@“123”];OCMVerifyAll(mockA);}使用场景:this这种方式创建的mock对象如果调用没有stub(stub代表存根)的方法会抛出异常。这需要确保每个独立调用的方法在mock的生命周期中都被存根。这种方法严格使用,很少使用。PartialMock创建的对象调用方法时:如果方法是stub,则调用stub之后的方法,如果方法不是stub,则调用原对象的方法,此方法仅限于mock实例对象。用法:-(void)testTalkPartialMock{idmockA=OCMPartialMock([Mennew]);Person*person1=[Personnew];person1.men=mockA;[person1talk:@"123"];OCMVerify([mockAlogstr:[OCMArgany]]);}使用场景:调用未存根的方法时,会调用实际对象的方法。当存根类的方法效果不佳时,此技术很有用。Men类中的+(NSString*)logstr:(NSString*)str会在调用testTalkPartialMock时执行打印操作。三种方法的区别表:(3)验证方法的调用方法:OCMVerify([mocksomeMethod]);OCMVerify(never(),[mockdoStuff]);//OCMVerify(times(n),[mockdoStuff]);//调用N次OCMVerify(atLeast(n),[mockdoStuff]);//至少调用N次OCMVerify(atMost(n),[mockdoStuff]);使用场景:在单元测试中可以验证一个方法是否执行,执行了多少次。延时验证调用:OCMVerifyAllWithDelay(mock,aDelay);使用场景:该函数用于等待异步操作,其中aDelay是期望的最长等待时间。(4)添加预期调用方法:准备数据:NSDictionary*info=@{@"name":@"momo"};idmock=OCMClassMock([MOOCMockDemoclass]);添加预期:OCMExpect([mockhandleLoadSuccessWithPerson:[OCMArgany]]);可以预期不会执行:OCMReject([mockhandleLoadFailWithPerson:[OCMArgany]]);可以验证参数://期望+参数验证OCMExpect([mockhandleLoadSuccessWithPerson:[OCMArgcheckWithBlock:^BOOL(idobj){MOPerson*person=(MOPerson*)obj;return[person.nameisEqualToString:@"momo"];}]]);Executionordercanbeexpected://以下方法预计按顺序执行[mocksetExpectationOrderMatters:YES];OCMExpect([mockhandleLoadSuccessWithPerson:[OCMArgany]]);OCMExpect([mockshowError:NO]);可以忽略参数(当预期方法执行时):OCMExpect([mockshowError:YES]).ignoringNonObjectArgs;//忽略参数执行:[MOOCMockDemohandleLoadFinished:info];断言:OCMVerifyAll(模拟);延迟断言:OCMVerifyAllWithDelay(mock,1);//支持延迟验证最后一个OCMVerifyAll会验证之前的期望是否有效,只要不调用就会出错。(5)参数约束调用方法:OCMStub([mocksomeMethodWithAnArgument:[OCMArgany]])OCMStub([mocksomeMethodWithPointerArgument:[OCMArganyPointer]])OCMStub([mocksomeMethodWithSelectorArgument:[OCMArganySelector]])使用场景:使用OCMVerify的()方法验证是否使用了方法调用。单元测试会验证方法参数是否一致。如果不一致,会提示验证失败。这时候如果只关注方法调用,不关注参数,可以使用[OCMArgany]传参。(6)网络接口的模拟,顾名思义,可以模拟网络接口的数据返回,测试不同数据下代码的方向性和准确性。调用方法:idmockManager=OCMClassMock([JDStoreNetworkclass]);[orderListVcsetComponentsNet:mockManager];[OCMStub([mockManagerstartWithSetup:[OCMArgany]didFinish:[OCMArgany]didCancel:[OCMArgany]])andDo:^(NSInvocation*invocation){void(^successBlock)(idcomponents,NSError*error)=nil;[调用getArgument:&successBlockatIndex:3];successBlock(@{@"code":@"1",@"resultCode":@"1",@"value":@{@"showOrderSearch":@"NO"}},nil);}];以上是调用setComponentsNet方法内部的接口,调用接口后可以模拟需要返回的数据就是successBlock中返回的测试数据。该方法是获取接口调用的方法签名,获取successBlock成功回??调参数,手动调用。也可以模拟接口失败,只要获取到签名中对应的失败回调即可实现。使用场景:在编写单元测试方法时,涉及到网络接口的模拟,mock接口通过这种方式返回结果。(7)恢复类替换类方法后,可以调用stopMocking将类恢复到原来的状态。调用方法:idclassMock=OCMClassMock([SomeClassclass]);/*dostuff*/[classMockstopMocking];使用场景:实例对象正常替换后,mock对象释放后会自动调用stopMocking,但是添加到类方法中的mock对象会跨越多次测试,替换后mock类对象不会被释放,以及模拟关系需要手动取消。(8)观察者模拟——创建一个接受通知的实例调用方法:-(void)testPostNotification{Person*person1=[[Personalloc]init];idobserverMock=OCMObserverMock();//为通知中心设置观察者[[NSNotificationCenterdefaultCenter]addMockObserver:observerMockname:@"name"object:nil];//设置观察期望[[observerMockexpect]notificationWithName:@"name"object:[OCMArgany]];//调用待验证方法[person1methodWithPostNotification];[[NSNotificationCenterdefaultCenter]removeObserver:observerMock];//调用并验证OCMVerifyAll(observerMock);}使用场景:创建一个模拟对象,可以用来观察通知。必须注册模拟才能接收通知。(9)Mock协议调用方法:idprotocolMock=OCMProtocolMock(@protocol(SomeProtocol));/*strictprotocol*/idclassMock=OCMstrictClassMock([SomeClassclass]);idprotocolMock=OCMstrictProtocolMock(@protocol(SomeProtocol));idprotocolMock=OCMProtocolMock(@protocol(SomeProtocol));/*严格协议*/idclassMock=OCMstrictClassMock([SomeClassclass]);idprotocolMock=OCMstrictProtocolMock(@protocol(SomeProtocol));调用场景:当需要创建实例时,当它具有协议定义的功能时使用。03mock使用限制对于同一个方法,先stub再expect是不行的:因为如果先stub,所有的调用都会变成stub,这样即使进程调用该方法,最后的OCMVerifyAll验证也会失败;解决方法是,在Bytheway,stubonOCMExpect,如:OCMExpect([mocksomeMethod]).andReturn(@"astring"),或者把stub放在expect之后。部分模拟不适用于某些类:例如NSString和NSDate,否则这些“免费桥接”类将抛出异常。有些方法不能stub:如:init、class、methodSignatureForSelector、forwardInvocation等。NSString和NSArray的类方法不能stub,否则无效。除非在子类中重写,否则无法验证NSObject方法调用。苹果核心类的私有方法调用无法验证,比如以_开头的方法。不支持延迟验证方法调用,目前只支持expect-run-verify方式的延迟验证。OCMock不支持多线程。最后,我希望本文和示例已经阐明了OCMock的一些最常见用途。OCMock站点:http://ocmock.org/features/是学习OCMock的最佳地点之一。模拟是单调的,但对于应用程序来说是必需的。如果一个方法难以用mock进行测试,则表明您的设计需要重新考虑。