好文分享:如何做好iOS单元测试

发表于:2022-10-17 09:36

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

 作者:xietao3    来源:稀土掘金

  前言
  平时写完业务代码的时候都会去自己测试一遍,后面每次有修改都需要重复测,不管是一个业务流程还是一个工具类,其实都可以通过测试框架来帮助我们完成测试,特别是一些频繁修改的代码,更需要严谨的测试。在浅浅地对自动化测试有一些了解时,觉得写测试代码挺耗时间,但其实对后期的帮助是非常大的,可以根据自己的实际情况来决定哪些地方需要加入自动化测试。
  单元测试
  1.1 加入测试 Target
  在新建项目时,勾选Include Unit Tests和Include UI Tests,即可为项目添加单元测试和 UI 测试。
  在添加测试代码时,你需要遵守一些最基本的规则:
  所有的测试类需要继承XCTestCase
  @interface TTTestCase : XCTestCase
  测试方法命名以 test 开始
  - (void)testThatMyFunctionWorks
  用 Assertion API 进行验证是否通过
  XCTAssertEqual(value, expectedValue)
  1.2 启动测试
  单元测试的结构:
  step1:准备输入;
  step2:运行正在测试的代码;
  step3:验证输出;
  // 准备输入
  NSString *dateString = @"2000-01-01";
  // 需要测试的方法
  BOOL isToday = [TTDateFormatter isTodayWithDateString:dateString];
  // 验证输出
  XCTAssert(isToday, @"isToday false");
  以上三个部分的代码准备完成后即可开始测试,启动的方式有很多种,可以根据你的实际情况选择以下方式:
  ·代码编辑器边栏菱形按钮,测试单个用例
  · Test 导航栏,测试单个用例
  · 快捷键? + U测试全部用例
  · 使用命令行工具 xcodebuild 可以测试单个用例,也可以测试全部用例。
  1.3 性能测试
  性能测试通过度量代码块执行所消耗的时间长短,来衡量是否通过测试。
  1.3.1 如何进行性能测试
  相关 API :
  measureBlock:
  - (void)testPerformanceOfMyFunction {
      [self measureBlock:^{
          // Do that thing you want to measure.
          MyFunction();
      }];
  }
  measureMetrics:automaticallyStartMeasuring:forBlock:
  - (void)testMyFunction2_WallClockTime {
      [self measureMetrics:[self class].defaultPerformanceMetrics automaticallyStartMeasuring:NO forBlock:^{
          // Do setup work that needs to be done for every iteration but you don't want to measure before the call to -startMeasuring
          SetupSomething();
          [self startMeasuring];
          // Do that thing you want to measure.
          MyFunction();
          [self stopMeasuring];
          // Do teardown work that needs to be done for every iteration but you don't want to measure after the call to -stopMeasuring
          TeardownSomething();
      }];
  }
  1.3.2 设置基准线
  所有的性能测试需要设置一个Baseline来验证是否通过测试,没有设置的会提示No baseline average for Time。
  我们可以通过点击measureBlock:方法左边菱形圆心 icon ,来设置Baseline,设置之后需要点击save保存。之后再执行测试用例时,如果成功,左边的icon会从圆心变成一个 。
  1.4 异步测试
  什么时候需要使用异步测试:
  ·打开文档
  · 在后台线程中执行的服务和网络活动
  · 执行动画
  · UI 测试时
  1.4.1 异步测试 XCTestExpectation
  异步测试分为3个部分: 新建期望 、 等待期望被履行 和 履行期望 。
  · XCTestExpectation :测试期望,可以由测试类持有,也可以自己持有,自己持有测试期望时灵活性更好一些,你可以选择等待哪些期望。
  // 测试类持有的初始化方法
  XCTestExpectation *expect1 = [self expectationWithDescription:@"asyncTest1"];
  // 自己持有的初始化方法
  XCTestExpectation *expect2 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest3"];
  · waitForExpectations:timeout: :等待异步的期望代码执行,根据初始化方式不同,等待的方法不同。
  // 测试类持有时的等待方法
  [self waitForExpectationsWithTimeout:10.0 handler:nil];
  // 自己持有时的等待方法
  [self waitForExpectations:@[expect3] timeout:10.0];
  · fulfill :履行期望,并且适当加入XCTAssertTrue等断言,来验证测试结果。
  XCTestExpectation *expect3 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest3"];
  [TTFakeNetworkingInstance requestWithService:apiRecordList completionHandler:^(NSDictionary *response) {
      XCTAssertTrue([response[@"code"] isEqualToString:@"200"]);
      [expect3 fulfill];
  }];
  [self waitForExpectations:@[expect3] timeout:10.0];
  1.4.2 异步测试 XCTWaiter
  XCTWaiter是 2017 年新增的异步测试方案,可以通过代理方式来处理异常情况。
  XCTWaiter *waiter = [[XCTWaiter alloc] initWithDelegate:self];
      
  XCTestExpectation *expect4 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest3"];
      
  [TTFakeNetworkingInstance requestWithService:@"product.list" completionHandler:^(NSDictionary *response) {
  XCTAssertTrue([response[@"code"] isEqualToString:@"200"]);
  expect4 fulfill];
  }];
  XCTWaiterResult result = [waiter waitForExpectations:@[expect4] timeout:10 enforceOrder:NO];
  XCTAssert(result == XCTWaiterResultCompleted, @"failure: %ld", result);
  XCTWaiterDelegate:如果委托是XCTestCase实例,下方代理被调用时会报告为测试失败。
  // 如果有期望超时,则调用。 
  - (void)waiter:(XCTWaiter *)waiter didTimeoutWithUnfulfilledExpectations:(NSArray<XCTestExpectation *> *)unfulfilledExpectations;
  // 当履行的期望被强制要求按顺序履行,但期望以错误的顺序被履行,则调用。
  - (void)waiter:(XCTWaiter *)waiter fulfillmentDidViolateOrderingConstraintsForExpectation:(XCTestExpectation *)expectation requiredExpectation:(XCTestExpectation *)requiredExpectation;
  // 当某个期望被标记为被倒置,则调用。 
  - (void)waiter:(XCTWaiter *)waiter didFulfillInvertedExpectation:(XCTestExpectation *)expectation;
  // 当 waiter 在 fullfill 和超时之前被打断,则调用。 
  - (void)nestedWaiter:(XCTWaiter *)waiter wasInterruptedByTimedOutWaiter:(XCTWaiter *)outerWaiter;
  1.5 查看测试结果
  在执行测试用例后,Xcode 会返回给我们测试结果,可以通过一下途径查看:
  ·Test 导航栏
  · Issue 导航栏
  · 代码编辑器左边栏
  · Report 导航栏
  除此之外,我们还可以在 Report 导航栏中查看更加详细的测试报告:
  · 测试通过/失败
  · 失败原因
  · 性能指标
  · 截屏
  · 嵌套的 activities
  · 测试覆盖率
  1.6 进行单元测试
  我新建一个时间工具类,帮助我转换时间,在使用之前,我们需要先进行测试,以保证功能完整且正确。
  这个工具类有以下 4 个公共方法,
  @interface TTDateFormatter : NSDate
  + (NSString *)stringFormatWithDate:(NSDate *)date;
  + (NSDate *)dateFormatWithString:(NSString *)dateString;
  + (BOOL)isTodayWithDateString:(NSString *)dateString;
  + (NSString *)getHowLongAgoWithTimeStamp:(NSTimeInterval)timeStamp;
  @end
  针对一个工具类的测试我们可以新建一个TTDateFormatterTests测试类,继承一个测试基类。再根据不同的方法写不同的测试方法。如果有if和switch等条件语句导致逻辑分支的代码,尽量使各个逻辑分支都能测试到,可以配合代码覆盖率来检查哪些逻辑分支未测试。
  @interface TTDateFormatterTests : TTTestCase
  @end
  @implementation TTDateFormatterTests
  - (void)testDateFormatter {
      NSString *originDateString = @"2018-06-06 20:20:20";
      NSDate *date = [TTDateFormatter dateFormatWithString:originDateString];
      NSString *dateString = [TTDateFormatter stringFormatWithDate:date];
      XCTAssertEqualObjects(dateString, originDateString);
  }
  - (void)testDateFormatterIsToday {
      NSString *dateString = [TTDateFormatter stringFormatWithDate:[NSDate date]];
      XCTAssertTrue([TTDateFormatter isTodayWithDateString:dateString]);
      XCTAssertFalse([TTDateFormatter isTodayWithDateString:@"2000-01-01"]);
  }
  - (void)testDateFormatterHowLongAgo {
  // 该方法中包含一个 switch ,要保证 switch 每个逻辑分支都测试到,所以需要多个测试。
      NSDate *now = [NSDate date];
      NSString *secAgo = [TTDateFormatter getHowLongAgoWithTimeStamp:now.timeIntervalSince1970 - 10 * sec];
      XCTAssertEqualObjects(secAgo, @"10秒前");
      
      NSString *minAgo = [TTDateFormatter getHowLongAgoWithTimeStamp:now.timeIntervalSince1970 - 15 * min];
      XCTAssertEqualObjects(minAgo, @"15分钟前");
      
      NSString *hourAgo = [TTDateFormatter getHowLongAgoWithTimeStamp:now.timeIntervalSince1970 - 20 * hour];
      XCTAssertEqualObjects(hourAgo, @"20小时前");
      NSString *dayAgo = [TTDateFormatter getHowLongAgoWithTimeStamp:now.timeIntervalSince1970 - 25 * hour];
      XCTAssertEqualObjects(dayAgo, @"1天前");
      NSString *daysAgo = [TTDateFormatter getHowLongAgoWithTimeStamp:now.timeIntervalSince1970 - 50 * hour];
      XCTAssertEqualObjects(daysAgo, @"2天前");
      NSString *longTimeAgo = [TTDateFormatter getHowLongAgoWithTimeStamp:1544002463];
      XCTAssertEqualObjects(longTimeAgo, @"2018-12-05 17:34:23");
  }
  @end
  合理使用测试基类和测试工具类,可以避免大量重复测试代码。时间转换工具类是一个没有外部依赖的类,当一些对外部有依赖的类需要测试时,可以尝试 OCMock ,它能帮助你模拟数据。另外,当你觉得测试框架提供的断言方法无法满足你时,也可以试着使用 OCHamcrest 。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号