iOS 单元测试之常用框架 OCMock 详解(1)

发表于:2022-8-18 09:29

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:王中文    来源:京东零售技术

  单元测试
  01单元测试的必要性
  测试驱动开发并不是一个很新鲜的概念了。在日常开发中,很多时候需要测试,但是这种输出是必须在点击一系列按钮之后才能在屏幕上显示出来的东西。测试的时候,往往是用模拟器一次一次的从头开始启动 app,然后定位到自己所在模块的程序,做一系列的点击操作,然后查看结果是否符合自己预期。
  这种行为无疑是对时间的巨大浪费。于是有很多资深工程师们发现,我们是可以在代码中构造一个类似的场景,然后在代码中调用我们之前想要检查的代码,并将运行结果和设想结果在程序中进行比较,如果一致,则说明我们的代码没有问题,由此就产生了单元测试。
  02单元测试的目的
  单元测试的主要目的是发现模块内部逻辑、语法、算法和功能错误。
  单元测试主要是基于白盒测试验证以下问题:
  ·验证代码与设计相符度。
  · 发现设计和需求中存在错误。
  · 发现在编码过程中引入的错误。
  单元测试关注的重点有以下部分:
  独立路径-对于基本执行路径和循环进行测试,可能的错误有:
  · 不同数据类型的比较。
  · “差1错”,即可能多循环或少循环一次。
  · 错误或不可能的终止条件。
  · 不适当的修改了循环变量。
  局部数据结构-单元的局部数据结构是最常见的错误来源,应设计测试用例以检查可能的错误:
  · 不一致的数据类型。
  · 检查不正确或不一致的数据类型。
  错误处理-比较完善的单元设计要能预见出错的条件,并设置适当的错误处理,以便在程序出错时,能对错误重新做安排,保证期逻辑上的正确性:
  · 出错的描述难以理解。
  · 显示的错误与实际的错误不符。
  · 对错误条件的处理不正确。
  边界条件-边界上出现错误是最常见的错误现象:
  · 取最大最小值发生错误。
  · 控制流中的大于、小于这些比较值常出现错误。
  单元接口-接口实际上就是输入和输出对应关系的集合,要对单元进行动态测试无非就是给这个单元一个输入,然后检查输出是否和预期一致。如果数据不能正常输入和输出,单元测试就无从谈起,因此需要对单元接口进行如下的测试:
  · 被测单元的输入、输出在个数、属性、顺序是否和详细设计中的描述一致。
  · 是否修改了只做输入用的形式参数。
  · 约束条件是否通过形式参数来传送。
  03单元测试依赖的两个主要框架
  OCUnit(即用 XCTest 进行测试)其实就是苹果自带的测试框架,主要是断言使用,由于使用简单本次文章不过多介绍。
  OCMock主要功能是模拟某个方法或者属性的返回值,你可能会疑惑为什么要这样做?使用模型生成的模型对象,再传进去不就可以了?答案是可以的,但是有特殊的情况,比如一些不容易构造或不容易获取的对象,此时你可以创建一个虚拟的对象来完成测试。实现思想是根据要mock的对象的class来创建一个对应的对象,并且设置好该对象的属性和调用预定方法后的动作(例如返回一个值,调用代码块,发送消息等等),然后将其记录到一个数组中,接下来开发者主动调用该方法,最后做一个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 的使用方法
  (一)置换方法(存根):告诉 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会执行打印操作。
  三种方式的差异表格:
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号