1、序文
iOS的测验能够分为单元测验
与UI测验
,优异的测验能够协助咱们快速的检查代码问题,从而写出稳定性强的高质量代码
- 一切测验用例都以
test
最初,包括自建的用例 - 做测验咱们首要遵从下边3步:
备数据
–>调办法
–>做断言
-
Command + U
实行一切测验用例、菱形箭头 实行该办法中测验用例
代码覆盖率
- 默许是封闭的代码覆盖率的,需求先敞开
- 运行测验,完毕后会显示代码覆盖率
- 你还能够借助苹果提供的命令行东西
xccov
来生成代码覆盖率陈述;值得一提的是,xccov
还能输出 JSON 格式的陈述
2、单元测验(UITest)
2.1、逻辑测验
- 首先咱们在ViewController中准备一段要被测验的办法:
- 然后按三部曲测验正确性:
- 点击办法前菱形中的 播放按钮 实行测验,正确的会SUCCESS,错误的会FAILD并抛出
XCTAssertEqual
中预设的错误信息:(咱们将预期值210改成错的200看一下)
2.2、异步测验
- 准备异步办法
@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
- 自建一个异步测验用例,
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); }]; }
- 异步回来用时超越预设时间则会FAILD
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、惯例测验
- 准备压力测验办法
@implementation ViewController - (void)openCamera { for (int i = 0; i < 1000; i++) { NSLog(@"测验悉数实行完毕耗时"); } } @end
- 调用压力测验办法
- (void)testPerformanceExample { //这是一个功用测验用例示例 [self measureBlock:{ //把你想丈量的时间的代码放在这儿 self.vc = [ViewController new]; [self.vc openCamera]; }]; }
- 检查耗时,并可设置基准线
- 能够看到均匀用时
- 可设置基准线,超越答应的误差会飘红
- 可看到均匀散布状况,每次测越共同阐明功用稳定性越好
2.3.2、部分功用测验
- 增加办法,该办法能够作为待测验办法的前置条件
@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
- 自定义功用测验用例,运用
measureMetrics
办法来进行部分功用测验,参数@[XCTPerformanceMetric_WallClockTime]
(枚举值只要 XCTPerformanceMetric_WallClockTime 这一个),运用startMeasuring
与stopMeasuring
包裹待测验内容 - 咱们再将countNum办法也纳入测验,能够看到耗时大幅增加,直接超出预设的0.5秒要求而报错
3、UI测验
- 什么时分需求运用 UI 测验:
- 单元测验无法覆盖时的补充计划
- 单元测验更精准
- UI 测验覆盖面的更全
- UI 测验的过程:
- 与要测验或与逻辑有关的 UI 进行互动
- 验证 UIelements 特点和状况
3.1、Record UI Test
能够将你操作手机的行为记录下来,并且转换成代码,协助你快速生成 UI 测验代码,但智能程度有限,常常需求额外修改,但这也能为咱们提供很大协助;敞开办法:选中 UI 测验类,你能在下方看到一个小红点,点击小红点开端录制你的交互
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 控件,控件类型多样,可能是Button,Cell,Window等等;该类实例有许多模仿交互的办法,如tap
模仿用户点击事件,swipe
模仿滑动事件,typeText:
模仿用户输入内容
- 一个页面中控件以树状结构寄存,咱们能够经过 Accessibility identifer、label、title 等办法来定位对应的控件
// 需求勾选 Accessibility Enabled,并且在 Label 一栏填入 myBtn XCUIElement *myBtn = app.buttons[@"myBtn"]; // 模仿用户点击按钮 [myBtn tap]; // firstMatch 回来第一个契合的控件 XCUIElement *textView = app.textViews.firstMatch; // 模仿用户在 textView 输入内容 [textView typeText:@"input string"];
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测验流程
- 新建一个 UI 测验 Target
- 运用 Record UI Test 或 手写代码,
定位 UI 元素
,并且模仿用户交互事件
- 参加
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.cells
的count
特点,当满意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 导航栏看到成功信息,它会按照你设置的模块分别展现测验结果
假如测验失利,你能够看到哪些模块是成功的,和在哪些模块中失利了
除此之外,你还能够尝试多层嵌套,activity 里边嵌套 activity
4.3、截屏
在 UI 测验中有 2 种类型支撑经过代码截屏,分别是XCUIElement
和XCUIScreen
// 获取一个截屏目标
XCUIScreenshot *screenshot = [app screenshot];
// 实例化一个附件目标 并传入截屏目标
XCTAttachment *attachment = [XCTAttachment attachmentWithScreenshot:screenshot];
// 附件的存储战略 假如挑选 XCTAttachmentLifetimeDeleteOnSuccess 则测验成功的状况会被删去
attachment.lifetime = XCTAttachmentLifetimeKeepAlways;
// 设置一个名字 方便区别
attachment.name = @"MyScreenshot";
[self addAttachment:attachment];
在测验结束后,能够在 Report 导航栏中检查截图:
除此之外 Xcode 提供了主动截图的功用,能够协助咱们在每一个交互操作之后主动截图;此功用会产生很多截图,需求慎重运用,一般状况最好勾选Delete when each test succeeds
,需求在 Edit Scheme –> Test –> Options 中敞开
所以你能够依据你的需求挑选适当的截图战略
4.4、越过部分测验
在 Xcode 10 中新增功用,在 Edit Scheme -> Test -> Info -> Tests 中能够经过撤销勾选,来挑选越过部分测验用例;在 target 的 Options 选项中,Automatically includes new tests
,选项是默许勾选的,新建的测验文件会主动增加进去
4.5、测验用例的实行次序
默许状况下,测验用例实行的次序是按字母次序来实行的,按固定次序实行可能会使一些隐式的依靠联系无法被发现。现在有了随机的实行次序,就能够挖掘出那些隐式的依靠联系;能够在 Edit Scheme -> Test -> Info -> Tests -> Options 中敞开该功用
4.6、并行测验
并行测验能够一起进行多个测验,从而节约很多时间。在测验时会发动多个模仿器,模仿器之间的数据都是阻隔的,能够在 Edit Scheme -> Test -> Info -> Tests -> Options 中敞开该功用
对于并行测验的一些主张:
- 某个测验用例需求耗费很多时间的类,能够拆分成多个类并行测验,从而节约时间
- 你需求清楚哪些测验在并行实行时是不安全的,避免并行实行这些测验
- 功用测验的能够统一放在一个 Bundle 中,禁用并行实行
5、OCMock
依靠注入
,经过模仿完成单一变量原则,控制变量;原理类似KVO的动态子类
ocmock用来虚拟类及办法的调用。正常状况能够不需求此mock,但在特别状况下能够进行mock以越过某些过程
- 安装
source 'https://github.com/CocoaPods/Specs.git' target 'MockDemoTests' do pod 'OCMock' #在test target下运用 end
- 生成Mock目标
- OCMClassMock
优先调用stub实例办法,未找到调用stub类办法,不调用本来办法
- OCMPartialMock
优先调用stub实例办法,不能调用stub类办法,不然调用本来的实例办法,不能调用本来类办法,不满意条件无法验证经过
- OCMStrictClassMock
只能调用stub办法,不然OCMVerifyAll(mockA)会抛出异常
- OCMClassMock
- 置换办法
调用该办法不会走具体的完成,直接运用return值替换
id xxxClass = [OCMock mockForClass[XXX class]]; [OCMStub([xxxClass method:[OCMArg any])andReturn(@"")];
- 验证办法的调用
OCMVerify([mock someMethod]);
- 增加预期
OCMExpect([mock handleLoadSuccessWithPerson:[OCMArg any]]);
参阅链接:/post/684490…