一、UI测试简介
1.1、什么是UITesting
2015 年,Apple 发布了 UI 自动化测试框架 XCUITest 并集成在 Xcode7 中,而 iOS/macOS UI 自动化测试依赖两个核心技术:XCUITest 和 Accessibility。
XCUITest 是集成在 Xcode 中的测试框架,若想使用 UI 测试功能,可以在创建 iOS 项目时勾选 Include Tests 选项,从而使项目具备自动化测试的能力。而 Accessibility 技术,则是 Apple 官方为视障用户提供的一整套使用 iOS/macOS App 的解决方案。
Xcode 项目创建 UITests Target 并运行测试,其编译产物 Test App 本质上是一个 Deamon 守护进程,该进程有独立的应用程序生命周期,依靠 XCUIApplication 类型进行管理。UITests 的 Test App 进程在运行时会驱动 Host App(项目的主 Target 产物),并且利用元素审查的相关 API 驱动 Host App 模拟用户行为交互,从而进行 UI 自动化测试。
对于 Accessibility 技术,开发人员需要注意的是,XCUITest 框架默认并不能将所有视图元素审查到,只会审查到可以被 VoiceOver 功能读取文字的元素。比如,UIButton 和 UILabel,这些视图对于视障用户而言可以通过语音来获知其内容,而对于 UIImageView、 UIView 这种对于视障人士并不友好的 UIKit 视图元素默认是不会审查到的,所以编码时要另行配置 Accessibility 相关属性,以保证其支持 Accessibility 从而在 UI 自动化查询的元素层级中可见。
基于 XCUITest 框架 和 Accessibility 技术的自动化测试,有利于 App 进行数据一致性校验,但 UI 一致性校验能力较弱。比如,App 可以针对某些数据请求结果或者某个元素是否存在进行校验,而视觉展示效果却仍需要人工介入。
1.2、使用UITesting
Using UI Testing:
- Complements unit testing(补充单元测试)
- Unit testing more precisely pinpoints failures(单元测试更精确地确定了失败)
- UI testing covers broader aspects of functionality(UI测试覆盖了函数边界方面)
- Find the right blend of UI tests and unit tests for your project(找到好的方式融合UI测试和单元测试)
Candidates for UI Testing(使用UI测试的情况):
- Demo sequences(一些列Demo)
- Common workflows(相同的工作流程)
- Custom views(相同的视图)
- Document creation, saving, and opening(文档的创建、保存、打开)
1.3、UI Recording
通过 UI Recording ,可以将你操作手机的行为记录下来,并且转换成代码,可以帮助你快速生成 UI 测试代码。选中 UI 测试类,你能再下方看到一个小红点,点击小红点开始录制你的交互。
在你进行交互时,Xcode 会自动转化成代码,你可以借此创建新的测试代码,也可以以此拓展已经存在的测试代码。当然它也不是十分完美,并不是总能如你所愿,还需要你做一些处理,比如说自动生成的代码过于繁琐,你可以用一些更简洁的代码实现。即使这样,UI Recording 也是非常高效的方式。点击下载Demo:ZJHUnitTestDemo
二、UI 测试相关的类
2.1、XCUITest 框架结构
XCUITest 测试框架 API 主要包含:元素查询(UI Element Queries)相关类型,如 XCUIElementQuery,UI 元素(UI Elements)相关类型,如 XCUIElement,以及测试 App 生命周期类型(Application Lifecycle)类型,如 XCUIApplication。
2.2、XCUIApplication
XCUIApplication 代表整个应用,可以用来启动、结束进程,或者传入一些启动参数,最常用的功能是利用 XCUIApplication
实例来查询 UI 上的元素。
// 返回 UI 测试 Target 设置中选中的 Target Application 的实例
- (instancetype)init;
// 根据 bundleId 返回一个应用程序实例
- (instancetype)initWithBundleIdentifier:(NSString *)bundleIdentifier;
// 启动应用程序
- (void)launch;
// 将应用程序唤醒至前台,在多程序联合测试下会用到
- (void)activate;
// 结束一个正在运行的应用程序
- (void)terminate;
2.3、XCUIElement
XCUIElement 应用程序中的 UI 控件,控件类型多样,可能是Button
,Cell
,Window
等等。该类实例有很多模拟交互的方法,如tap
模拟用户点击事件,swipe
模拟滑动事件,typeText:
模拟用户输入内容。在 UI 测试中我们需要找到某个空间,可以通过他们的类型来缩小范围。另外还有一种方式通过 Accessibility identifer, label, title 等等方式来定位对应的控件。通过类型加 identifier 的方式来定位的控件元素的方式,可以满足大多数场景。
也可以通过代码的方式添加Accessibility identifer。
// 通过代码的方式添加Accessibility identifer
for (int i = 0; i < 5; i++) {
UISwitch *swt = [UISwitch new];
CGFloat pointY = 50 * i + 100;
swt.center = CGPointMake(self.view.frame.size.width/2, pointY);
// 添加accessibility标记
swt.accessibilityLabel = [NSString stringWithFormat:@"swt-%d", I];
[self.view addSubview:swt];
}
// UI测试获取控件
XCUIApplication *app = [[XCUIApplication alloc] init];
[app launch];
XCUIElement *codeSwt1 = app.switches[@"swt-1"];
[codeSwt1 tap];
2.4、XCUIElementQuery
XCUIElementQuery 是一个用来定位控件元素的类,一般是一组符合筛选条件的元素集合。如app.buttons
即返回 XCUIElementQuery 实例,是包含了当前所有的button
的集合,你可以再通过 XCUIElementQuery
的方法做下一步的筛选。
使用 NSPredicate 为查询条件增加条件
// 查找所有的 collectionView 的 cell, collectionViews 和 cells 是 XCUIElementQuery 提供的方法
XCUIElementQuery *cells = app.collectionViews.cells;
// 使用 NSPredicate 为查询条件增加条件
XCUIElementQuery *cells = [app.collectionViews.cells matchingPredicate:[NSPredicate predicateWithFormat:@"identifier LIKE '?labelPrice?'"]];
三、UI测试示例
点击下载Demo:ZJHUnitTestDemo
3.1、使用UI Recording自动生成代码
新建一个 UI 测试 Target,使用 UI Recording 自动生成代码,或者也可以直接手写。
3.2、修改UI Recording 代码
UI Recording 的代码识别不出中文,需要手动改下;还会点击两次 tag,删除一个就好。
/// 修改 UI Recording 生成的代码
- (void)testLogin2 {
// 拿到当前application程序
XCUIApplication *app = [[XCUIApplication alloc] init];
// 点击 "UITestDemo" 按钮
[app.staticTexts[@"UITestDemo"] tap];
// 点击账号textField
[[[[[[[[[app.windows childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:0] tap];
// 点击键盘 shift,切换大小写
[app.buttons[@"shift"] tap];
// 点击键盘 a
XCUIElement *aKey = app.keys[@"a"];
[aKey tap];
// [aKey tap]; // 多余tag 需要注释掉
// 点击密码textField
[[[[[[[[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:1] tap];
// 切换数字键盘
XCUIElement *moreKey = app.keys[@"more"];
[moreKey tap];
// 输入1、2、3、4
XCUIElement *key = app.keys[@"1"];
[key tap];
XCUIElement *key2 = app.keys[@"2"];
[key2 tap];
XCUIElement *key3 = app.keys[@"3"];
[key3 tap];
XCUIElement *key4 = app.keys[@"4"];
[key4 tap];
// 点击登录按钮
XCUIElement *button = app.buttons[@"登录"];
[button.staticTexts[@"登录"] tap];
// 点击键盘删除按钮
XCUIElement *deleteKey = app.keys[@"delete"];
[deleteKey tap];
// 点击登录按钮
[button tap];
// 点击返回按钮
[app.navigationBars[@"Record List"].buttons[@"登录"] tap];
}
3.3、精简代码
UI Recording 生成的代码还不够简练,可以再次对其修改。也可以直接编写,不使用UI Recording生成的
/// 精简代码
- (void)testLogin3 {
// 拿到当前application程序
XCUIApplication *app = [[XCUIApplication alloc] init];
// 获取 “UITestDemo” 按钮,并点击,跳转到登录页面
[app.staticTexts[@"UITestDemo"] tap];
// 拿到当前app下的textfeild的搜索器
XCUIElementQuery *tfQuery = app.textFields;
// 账号textField
XCUIElement *accountTF = [tfQuery elementBoundByIndex:0];
// 密码textField
XCUIElement *passwordTF = [tfQuery elementBoundByIndex:1];
// 拿到当前app下的button的搜索器
XCUIElementQuery *btnQuery = app.buttons;
// 获取登录按钮
XCUIElement *loginBtn = btnQuery[@"登录"];
// 模拟UI操作
[accountTF tap]; // 点击账号textField
[accountTF typeText:@"a"]; // 输入字母a
[passwordTF tap];// 点击密码textField
[passwordTF typeText:@"1234"]; // 输入字母123456
[loginBtn tap]; // 点击登录,提示密码错误
// 获取键盘的删除按钮
XCUIElement *deleteBtn = app.keys[@"delete"];
[deleteBtn tap]; // 点击一次删除按钮
// 再次点击登录按钮
[loginBtn tap]; // 点击登录,成功跳转
// 获取 “Record List” navigationBar,
XCUIElement *navBarElement = app.navigationBars[@"Record List"];
// 获取返回按钮
XCUIElement *backBtn = navBarElement.buttons[@"登录"];
// 点击返回按钮
[backBtn tap];
}
注意:如果某些UI测试失败,请禁用“连接硬件键盘”选项。
为此,请在模拟器应用程序中选择“ I / O”菜单选项,然后转到
Keyboard
并取消选中Connect hardware keyboard
。 连接硬件键盘后,UI测试似乎无法访问模拟器中的text field
。
四、UI测试拓展 Tips
4.1、等待预期
可以用expectationForPredicate:evaluatedWithObject:handler:
方法监听对象属性,当满足
NSPredicate条件时,
expectation相当于自动
fullfill`。如果一直不满足条件,会一直等待直至超时,除此之外还可以用通知和 KVO 的方式实现。
例如,列表中,新增一个cell数据后,可以监听监听app.cells
的count
属性,判断cell的个数是否按预期增加,代码如下:
// 暂存当前 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];
// 执行添加操作,或者网络请求等异步操作
[self addCellData];
// 等待实现预期,这里等到10s
[self waitForExpectationsWithTimeout:10 handler:nil];
4.2、多应用联合测试
多应用联合测试时,依赖XCUIApplication
类的以下 2 个方法:
- initWithBundleIdentifier:
- activate
前者可以根据 BundleId 获取其他 App 的实例,让我们可以启动其他 App。后者可以让 App 从后台切换至前台,在多应用间切换。简单实现代码如下:
- (void)testExample {
// 返回 UI 测试 Target 设置中选中的 Target Application 的实例
XCUIApplication *app = [[XCUIApplication alloc] init];
// 使用 BundleId 获得另外一个 App 实例:需要先创建另个测试app
XCUIApplication *anotherApp = [[XCUIApplication alloc] initWithBundleIdentifier:@"zjh.ZJHUnitTestDemo2"];
// 先启动我们的主 App
[app launch];
// 做一系列测试1
[app.staticTexts[@"UITestDemo"] tap];
[app.navigationBars[@"登录"].buttons[@"Home"] tap];
sleep(2);
// 启动另一个 App
[anotherApp activate];
sleep(2);
// 回到我们的主 App (在 App 未启动的情况下调 activate 会让 App 启动)
[app activate];
// 做一系列测试2
[app.staticTexts[@"UITestDemo"] tap];
[app.navigationBars[@"登录"].buttons[@"Home"] tap];
}
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、被测试 app 如何判断正在进行 UI Test
在启动 app 时增加一个启动参数,在 app 中读取。
// 测试代码
XCUIApplication *app = [[XCUIApplication alloc] init];
app.launchEnvironment = @{@"isUITest" : @YES};
[app launch];
// app 代码
+ (BOOL)isUITesting {
NSDictionary *environment = [[NSProcessInfo processInfo] environment];
return [environment[@"isUITest"] boolValue];
}
五、Accessibility Inspector简介
5.1、使用 Accessibility Inspector
Accessibility Inspector 辅助功能检查器,通过辅助功能检查器,您可以识别应用程序中无法访问的部分。它提供了有关如何访问它们的反馈,并模拟画外音,以帮助您识别画外音用户的体验。观看在辅助功能检查器中完全调试的应用程序的实时演示,并了解如何利用这个强大的工具使您的应用程序更适合每个人。
前文中提到 Apple 对于视图元素会默认审查能够通过 VoiceOver 播放文字的视图元素,而对于 UIImageView、UIView 这种默认不支持 Accessibility 功能的需要配置相关特性,而开发人员在开发过程中可以通过 Accessibility Inspector 查看不同进程的 Accessibility 元素层级,该应用可以审查 iOS 和 macOS 的元素。
选择 Xcode 的图标菜单并选择 Open Developer Tool 选项,点击 Accessibility Inspector 即可开始使用。
当我们没有设置 isAccessibilityElement 属性时,在 Accessibility 元素层级结构中就无法看到 UIImageView 和 UIView 元素,只能看到 “t我是Button” 和“我是Label”。而当我们将 UIView 的 isAccessibilityElement 属性设置为 YES 时, UIView 元素才能在元素层级中可见,UIImageView默认还是看不见。设置代码如下:
NSArray *nameArr = @[@"我是Button", @"我是Label", @"我是View", @"我是Image"];
if (i == 0) { // 按钮
UIButton *btn = [[UIButton alloc] initWithFrame:btnF];
[btn setTitle:nameArr[i] forState:UIControlStateNormal];
temView = btn;
} else if (i == 1) { // label
UILabel *lab = [[UILabel alloc] initWithFrame:btnF];
lab.text = nameArr[i];
lab.textAlignment = NSTextAlignmentCenter;
temView = lab;
} else if (i == 2) { // view
UIView *view = [[UIView alloc] initWithFrame:btnF];
view.isAccessibilityElement = YES; // 将 UIView 的 isAccessibilityElement 属性设置为 YES
view.accessibilityIdentifier = nameArr[I];
temView = view;
} else if (i == 3) { // 图片
UIImageView *imgView = [[UIImageView alloc] initWithFrame:btnF];
imgView.image = [UIImage imageNamed:@"avatar"];
imgView.accessibilityIdentifier = nameArr[i];
temView = imgView;
}
5.2、Accessibility 相关属性
@property (nullable, nonatomic, copy) NSString *accessibilityLabel;
accessibilityLabel 属性可以解决绝大部分的 Accessibility 问题,当光标将焦点放在设置该属性的元素师时,它的内容可由 VoiceOver 读取的人类可读的字符串。但如果不是需要被视障用户获知的视图元素,仅用于自动化测试,就可以不用设置该属性。
@property(nullable, nonatomic, copy) NSString *accessibilityIdentifier API_AVAILABLE(ios(5.0));
accessibilityIdentifier 属性不会被 VoiceOver 诵读,而是面向开发人员的字符串,可在不希望用户操作 accessibilityLabel 的情况下使用。
@property (nonatomic) BOOL isAccessibilityElement;
如果 isAccessibilityElement 未设置为 true,那么这个视图将不会在 Accessibility 视图层次结构中可见。
- The default value for this property is false unless the element is a standard UIKit control, in which case, the value is true. —— Apple Documentation
另外,根据 Apple 官方中的介绍 UIControl 的子类的 isAccessibilityElement 属性都默认设置为 true。
5.3、编写测试用例
- (void)testExample {
XCUIApplication *app = [[XCUIApplication alloc] init];
[app.staticTexts[@"Accessibility Demo"] tap];
XCUIElement *button = app.buttons[@"我是Button"];
XCTAssertTrue(button.exists);
XCUIElement *label = app.staticTexts[@"我是Label"];
XCTAssertTrue(label.exists);
XCUIElement *view = app.otherElements[@"我是View"];
XCTAssertTrue(view.exists);
XCUIElement *imgview = app.images[@"我是Image"];
XCTAssertTrue(imgview.exists);
}
六、三方框架KIF简介
6.1、KIF简介
KIF 的全称是Keep it functional。它是一个建立在XCTest的UI测试框架,通过accessibility来定位具体的控件,再利用私有的API来操作UI。由于是建立在XCTest上的,所以你可以完美的借助XCode的测试相关工具。
6.2、pod引入框架
必须将Target设置为Unit Test,根据GitHub官方说明。不要设置成UI Test 项目了,我这就设错了,调了大半天才找到原因
查看GitHub的ReadMe,使用Cocoapod进行安装,命令如下(在Debug模式下才生效)
-
KIF一定要放到测试项目下面
target 'ZJHKIFUnitTestDemoTests' do pod 'KIF', :configurations => ['Debug'] end
6.3、简单使用
更多接口介绍,可参考:KIF API中文翻译
参考链接:
iOS 单元测试和 UI 测试快速入门:https://juejin.cn/post/6844903744170098695
iOS UI 自动化测试原理以及在 Trip.com 的应用实践:https://www.51cto.com/article/686176.html
iOS UI Testing 指北:https://nixwang.com/2018/09/30/ios-ui-testing/