1、序文

iOS的测验能够分为单元测验UI测验,优异的测验能够协助咱们快速的检查代码问题,从而写出稳定性强的高质量代码

iOS-测试

  • 一切测验用例都以test最初,包括自建的用例
  • 做测验咱们首要遵从下边3步:备数据 –> 调办法 –> 做断言
  • Command + U实行一切测验用例、菱形箭头 实行该办法中测验用例

代码覆盖率

  1. 默许是封闭的代码覆盖率的,需求先敞开
    iOS-测试
  2. 运行测验,完毕后会显示代码覆盖率
    iOS-测试
  3. 你还能够借助苹果提供的命令行东西xccov来生成代码覆盖率陈述;值得一提的是,xccov还能输出 JSON 格式的陈述

2、单元测验(UITest)

2.1、逻辑测验

  1. 首先咱们在ViewController中准备一段要被测验的办法:
    iOS-测试
  • 然后按三部曲测验正确性:
    iOS-测试
  • 点击办法前菱形中的 播放按钮 实行测验,正确的会SUCCESS,错误的会FAILD并抛出XCTAssertEqual中预设的错误信息:(咱们将预期值210改成错的200看一下)
    iOS-测试

2.2、异步测验

  1. 准备异步办法
    @implementation ViewController
    - (void)loadData:(void ()(id))dataBlock {
        dispatch_async(dispatch_get_global_queue(0, 0), {
            [NSThread sleepForTimeInterval:2];
            NSString *dataStr = @"loadData";
            dispatch_async(dispatch_get_main_queue(), {
                dataBlock(dataStr);
            });
        });
    }
    @end
    
  2. 自建一个异步测验用例,XCTestExpectation设置希望,waitForExpectationsWithTimeout设置异步等候时间,fulfill实行希望,将希望使用到整个异步办法
    - (void)testAsync {
        self.vc = [ViewController new];
        // 设置希望
        XCTestExpectation *ec = [self expectationWithDescription:@"没有到达希望"];
        // 调异步办法
        [self.vc loadData:(id data) {
            // 做断言
            XCTAssertNotNil(data);
            // 将希望使用在整个异步
            [ec fulfill];
        }];
        // 设置容许等候时长
        [self waitForExpectationsWithTimeout:2 handler:(NSError * _Nullable error) {
            NSLog(@"error = %@",error);
        }];
    }
    
  3. 异步回来用时超越预设时间则会FAILD
    iOS-测试
XCTWaiter

署理办法来处理异常状况:

- (void)testAsync {
  self.vc = [ViewController new];
  XCTWaiter *waiter = [[XCTWaiter alloc] initWithDelegate:self];
  XCTestExpectation *ec = [[XCTestExpectation alloc] initWithDescription:@"没有到达希望"];
  [self.vc loadData:(id data) {
    XCTAssertNotNil(data);
    [ec fulfill];
  }];
  XCTWaiterResult result = [waiter waitForExpectations:@[ec] timeout:3 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;

2.3、功用测验

2.3.1、惯例测验
  1. 准备压力测验办法
    @implementation ViewController
    - (void)openCamera {
        for (int i = 0; i < 1000; i++) {
            NSLog(@"测验悉数实行完毕耗时");
        }
    }
    @end
    
  2. 调用压力测验办法
    - (void)testPerformanceExample {
        //这是一个功用测验用例示例
        [self measureBlock:{
            //把你想丈量的时间的代码放在这儿
            self.vc = [ViewController new];
            [self.vc openCamera];
        }];
    }
    
  3. 检查耗时,并可设置基准线
    iOS-测试
    • 能够看到均匀用时
    • 可设置基准线,超越答应的误差会飘红
    • 可看到均匀散布状况,每次测越共同阐明功用稳定性越好
2.3.2、部分功用测验
  1. 增加办法,该办法能够作为待测验办法的前置条件
     @implementation ViewController
    - (void)countNum {
        for (int i = 0; i < 1000; i++) {
            _num++;
            NSLog(@"countNum = %d",_num);
        }
    }
    - (void)openCamera {
        for (int i = 0; i < 1000; i++) {
            NSLog(@"测验悉数实行完毕耗时%d",_num);
        }
    }
    @end
    
  2. 自定义功用测验用例,运用measureMetrics办法来进行部分功用测验,参数@[XCTPerformanceMetric_WallClockTime](枚举值只要 XCTPerformanceMetric_WallClockTime 这一个),运用startMeasuringstopMeasuring包裹待测验内容
    iOS-测试
  3. 咱们再将countNum办法也纳入测验,能够看到耗时大幅增加,直接超出预设的0.5秒要求而报错
    iOS-测试

3、UI测验

  • 什么时分需求运用 UI 测验:
    • 单元测验无法覆盖时的补充计划
    • 单元测验更精准
    • UI 测验覆盖面的更全
  • UI 测验的过程:
    1. 与要测验或与逻辑有关的 UI 进行互动
    2. 验证 UIelements 特点和状况

3.1、Record UI Test

能够将你操作手机的行为记录下来,并且转换成代码,协助你快速生成 UI 测验代码,但智能程度有限,常常需求额外修改,但这也能为咱们提供很大协助;敞开办法:选中 UI 测验类,你能在下方看到一个小红点,点击小红点开端录制你的交互

iOS-测试

3.2、测验相关类

3.2.1、XCUIApplication

XCUIApplication能够回来一个使用程序实例,然后你就能够经过测验代码发动使用程序

// 回来 UI 测验 Target 设置中选中的 Target Application 的实例
- (instancetype)init;
// 依据 bundleId 回来一个使用程序实例
- (instancetype)initWithBundleIdentifier:(NSString *)bundleIdentifier;
// 发动使用程序
- (void)launch;
// 将使用程序唤醒至前台,在多程序联合测验下会用到 
- (void)activate;
// 结束一个正在运行的使用程序
- (void)terminate;
3.2.2、XCUIElement

使用程序中的 UI 控件,控件类型多样,可能是ButtonCellWindow等等;该类实例有许多模仿交互的办法,如tap模仿用户点击事件,swipe模仿滑动事件,typeText:模仿用户输入内容

  • 一个页面中控件以树状结构寄存,咱们能够经过 Accessibility identiferlabeltitle 等办法来定位对应的控件
    // 需求勾选 Accessibility Enabled,并且在 Label 一栏填入 myBtn
    XCUIElement *myBtn = app.buttons[@"myBtn"];
    // 模仿用户点击按钮
    [myBtn tap];
    // firstMatch 回来第一个契合的控件 
    XCUIElement *textView = app.textViews.firstMatch; 
    // 模仿用户在 textView 输入内容 
    [textView typeText:@"input string"];
    
    iOS-测试
3.2.3、XCUIElementQuery

一切满意挑选条件的调集,如app.buttons回来包括了当前一切的button的调集 XCUIElementQuery

XCUIApplication *app = [[XCUIApplication alloc] init];
[app launch];
// id为"login"的NavigationBar中的element
XCUIElement *element = [[app.otherElements containingType:XCUIElementTypeNavigationBar identifier:@"login"] childrenMatchingType:XCUIElementTypeOther].element;
// element中的一切Button
XCUIElementQuery *btnQueue = [element childrenMatchingType:XCUIElementTypeButton];
// 一切Button中的第一个
XCUIElement *myBtn = [btnQueue elementBoundByIndex:0];

XCUIElementQuery 常见定位元素的办法:

  • count:匹配的数量;

    // 当 navigationBars 的 count 等于 1 时,你能够直接定位到 navigationBar
    app.navigationBars.element

  • subscripting:经过 id 来定位

    table.staticTexts[“Groceries”]

  • index:经过元素的下标来定位

    table.staticTexts.elementAtIndex(0)

3.3、UI测验流程

  1. 新建一个 UI 测验 Target
  2. 运用 Record UI Test手写代码定位 UI 元素,并且模仿用户交互事件
  3. 参加XCTAssert等断言逻辑,验证测验是否经过
    let app = XCUIApplication()
    // 发动 app
    app.launch()
    // 定位元素
    let myBtn = app.buttons["myBtn"]
    // 模仿用户交互事件
    myBtn.tap()
    // 验证测验是否经过
    XCTTAssertionEqual(app.tables.cells.count, 1)
    

3.4、示例

// 测验主流程
- (void)testMainFlow {
    // 发动 app
    XCUIApplication *app = [[XCUIApplication alloc] init];
    [app launch];
    // 增加笔记
    [self addRecordWithApp:app msg:@"今天天气真好!"];
    [self addRecordWithApp:app msg:@"今天詹姆斯特别给力,带领球队走向成功。✌️"];
    while (app.cells.count > 0) {
    	  // 删去笔记  
        [self deleteFirstRecordWithApp:app];
    }
}
/**
 增加笔记
 @param app app 实例
 @param msg 笔记内容
 */
- (void)addRecordWithApp:(XCUIApplication *)app msg:(NSString *)msg {
    // 暂存当前 cell 数量
    NSInteger cellsCount = app.cells.count;
    // 设置一个预期 判别 app.cells 的 count 特点会等于 cellsCount+1, 等候直至失利,假如契合则不再等候
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"count == %d",cellsCount+1];
    [self expectationForPredicate:predicate evaluatedWithObject:app.cells handler:nil];
    // 定位导航栏+号按钮,点击进入增加笔记页面 
    XCUIElement *addButton = app.navigationBars[@"Record List"].buttons[@"Add"];
    [addButton tap];
    // 测验 未输入任何内容点击保存
    [app.navigationBars[@"Write Anything"].buttons[@"Save"] tap];
    // 定位文本输入框 输入内容
    XCUIElement *textView = app.textViews.firstMatch;
    [textView typeText:msg];
    // 保存
    [app.navigationBars[@"Write Anything"].buttons[@"Save"] tap];
    // 等候预期
    [self waitShortTimeForExpectations];
}
/**
 删去最近一个笔记
 @param app app 实例
 */
- (void)deleteFirstRecordWithApp:(XCUIApplication *)app {
    NSInteger cellsCount = app.cells.count;
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"count == %d",cellsCount-1];
    // 设置一个预期 判别 app.cells 的 count 特点会等于 cellsCount-1, 等候直至失利,假如契合则不再等候
    [self expectationForPredicate:predicate evaluatedWithObject:app.cells handler:nil];
    // 定位到 cell 元素
    XCUIElement *firstCell = app.cells.firstMatch;
    // 左滑出现删去按钮
    [firstCell swipeLeft];
    // 定位删去按钮
    XCUIElement *deleteButton = [app.buttons matchingIdentifier:@"Delete"].firstMatch;
    // 点击删去按钮
    if (deleteButton.exists) {
        [deleteButton tap];        
    }
    // 等候预期
    [self waitShortTimeForExpectations];
}

在上面的逻辑中涉及到异步的恳求,咱们能够经过利用expectationForPredicate:evaluatedWithObject:handler:办法监听app.cellscount特点,当满意NSPredicate条件时,expectation相当于主动fullfill;假如一向不满意条件,会一向等候直至超时,除此之外还能够用通知和 KVO 的办法完成

4、拓展

4.1、多使用联合测验

多使用联合测验时,依靠XCUIApplication类的以下 2 个办法:

  • initWithBundleIdentifier:
  • activate

前者能够依据 BundleId 获取其他 App 的实例,让咱们能够发动其他 App;后者能够让 App 从后台切换至前台,在多使用间切换;简单完成代码如下:

// 回来 UI 测验 Target 设置中选中的 Target Application 的实例
XCUIApplication *ttApp = [[XCUIApplication alloc] init];
// 运用 BundleId 取得另外一个 App 实例
XCUIApplication *anotherApp = [[XCUIApplication alloc] initWithBundleIdentifier:@"Another.App.BundleId"];
// 先发动咱们的主 App
[ttApp launch];
// 做一系列测验
// 发动另一个 App
[anotherApp launch];
// 做一系列测验
// 回到咱们的主 App (在 App 未发动的状况下调 activate 会让 App 发动)
[ttApp activate];

4.2、逻辑杂乱场景下的 Activities

在一些逻辑比较杂乱的测验中,咱们能够借助XCTContext类来帮咱们把测验逻辑分割成多个小的测验模块;比如说咱们有一个业务,关联多个模块,这个时分咱们能够用类似下面的代码来处理:

// 模块 1
[XCTContext runActivityNamed:@"step1" block:(id<XCTActivity>  _Nonnull activity) {
    XCTestExpectation *expect1 = [self expectationWithDescription:@"asyncTest1"];
    [TTFakeNetworkingInstance requestWithService:apiRecordSave completionHandler:(NSDictionary *response) {
        XCTAssertTrue([response[@"code"] isEqualToString:@"200"]);
        [expect1 fulfill];
    }];
}];
// 模块 2
[XCTContext runActivityNamed:@"step2" block:(id<XCTActivity>  _Nonnull activity) {
    XCTestExpectation *expect2 = [self expectationWithDescription:@"asyncTest2"];
    [TTFakeNetworkingInstance requestWithService:apiRecordDelete completionHandler:(NSDictionary *response) {
        XCTAssertTrue([response[@"code"] isEqualToString:@"200"]);
        [expect2 fulfill];
    }];
}];
[self waitShortTimeForExpectations];

假如测验成功,能够在 Report 导航栏看到成功信息,它会按照你设置的模块分别展现测验结果

iOS-测试

假如测验失利,你能够看到哪些模块是成功的,和在哪些模块中失利了

iOS-测试

除此之外,你还能够尝试多层嵌套,activity 里边嵌套 activity

4.3、截屏

在 UI 测验中有 2 种类型支撑经过代码截屏,分别是XCUIElementXCUIScreen

// 获取一个截屏目标
XCUIScreenshot *screenshot = [app screenshot];
// 实例化一个附件目标 并传入截屏目标
XCTAttachment *attachment = [XCTAttachment attachmentWithScreenshot:screenshot];
// 附件的存储战略 假如挑选 XCTAttachmentLifetimeDeleteOnSuccess 则测验成功的状况会被删去
attachment.lifetime = XCTAttachmentLifetimeKeepAlways;
// 设置一个名字 方便区别
attachment.name = @"MyScreenshot";
[self addAttachment:attachment];

在测验结束后,能够在 Report 导航栏中检查截图:

iOS-测试

除此之外 Xcode 提供了主动截图的功用,能够协助咱们在每一个交互操作之后主动截图;此功用会产生很多截图,需求慎重运用,一般状况最好勾选Delete when each test succeeds,需求在 Edit Scheme –> Test –> Options 中敞开

iOS-测试

所以你能够依据你的需求挑选适当的截图战略

4.4、越过部分测验

在 Xcode 10 中新增功用,在 Edit Scheme -> Test -> Info -> Tests 中能够经过撤销勾选,来挑选越过部分测验用例;在 target 的 Options 选项中,Automatically includes new tests,选项是默许勾选的,新建的测验文件会主动增加进去

iOS-测试

4.5、测验用例的实行次序

默许状况下,测验用例实行的次序是按字母次序来实行的,按固定次序实行可能会使一些隐式的依靠联系无法被发现。现在有了随机的实行次序,就能够挖掘出那些隐式的依靠联系;能够在 Edit Scheme -> Test -> Info -> Tests -> Options 中敞开该功用

iOS-测试

4.6、并行测验

并行测验能够一起进行多个测验,从而节约很多时间。在测验时会发动多个模仿器,模仿器之间的数据都是阻隔的,能够在 Edit Scheme -> Test -> Info -> Tests -> Options 中敞开该功用

iOS-测试

对于并行测验的一些主张:

  • 某个测验用例需求耗费很多时间的类,能够拆分成多个类并行测验,从而节约时间
  • 你需求清楚哪些测验在并行实行时是不安全的,避免并行实行这些测验
  • 功用测验的能够统一放在一个 Bundle 中,禁用并行实行

5、OCMock

依靠注入,经过模仿完成单一变量原则,控制变量;原理类似KVO的动态子类

ocmock用来虚拟类及办法的调用。正常状况能够不需求此mock,但在特别状况下能够进行mock以越过某些过程

  1. 安装
    source 'https://github.com/CocoaPods/Specs.git' 
        target 'MockDemoTests' do
            pod 'OCMock' #在test target下运用
        end
    
  2. 生成Mock目标
    • OCMClassMock

      优先调用stub实例办法,未找到调用stub类办法,不调用本来办法

    • OCMPartialMock

      优先调用stub实例办法,不能调用stub类办法,不然调用本来的实例办法,不能调用本来类办法,不满意条件无法验证经过

    • OCMStrictClassMock

      只能调用stub办法,不然OCMVerifyAll(mockA)会抛出异常

  3. 置换办法

    调用该办法不会走具体的完成,直接运用return值替换

    id xxxClass = [OCMock mockForClass[XXX class]];
    [OCMStub([xxxClass method:[OCMArg any])andReturn(@"")];
    
  4. 验证办法的调用
    OCMVerify([mock someMethod]);
    
  5. 增加预期
    OCMExpect([mock handleLoadSuccessWithPerson:[OCMArg any]]);
    

参阅链接:/post/684490…