了解使用iOS,Xcode 9和Swift的UI测试
Xcode提供了功能齐全的可编写脚本的UI测试框架。 使用该框架的关键是了解其架构以及如何最好地利用其功能。
了解Xcode UI测试
在Xcode中创建新项目时,新项目向导会询问您是否要包含单元测试 ,以及是否要包含UI测试 。
一个人可能会怀疑-UI测试不是单元测试吗? 如果没有,那是什么?
实际上,这些复选框及其结果主要是用来通知Xcode在项目中创建哪些目标的。 选中每个复选框都会在项目中生成不同类型的测试目标。
Xcode 单元测试和Xcode UI测试之间的根本区别:
- 单元测试用于测试源代码是否产生预期的结果。 例如:确保函数在传递特定参数时会生成某些预期结果。
- UI测试测试用户界面是否按预期方式运行。 例如:UI测试可能会以编程方式点击一个按钮,该按钮会自动切换到新屏幕,然后以编程方式检查预期的屏幕是否确实加载并包含预期的内容。
单元测试和UI测试都支持完全自动化,并支持在应用程序生命周期内进行回归测试。
一般而言,Xcode 单元测试将练习并评估您的Swift代码 ,但不检查对用户界面的影响,而Xcode UI测试则评估响应操作的UI行为 ,但不检查您的代码。
与往常一样,这些是带有例外的通用语句。 当然,可以从(代码)单元测试中了解UI,并可以从UI测试中了解代码对象的状态。 但是,这些工具旨在根据此概括性说明使用。
UI测试示例
在检查UI测试的体系结构和实现之前,让我们看一下已完成的操作测试。 此测试的用户故事如下:
在第一个屏幕上,用户可以在表格视图中选择一个单元格,这将打开第二个表格,在标签中显示所选值。 然后,用户可以在原始标签下方的文本框中键入新值。 当用户随后返回第一个表单时,新值将显示在“表视图”中。
如果质量检查测试人员要手动检查此过程,则会执行以下顺序:
- 启动应用
- 连续点击
- 加载时观察表格视图行文本在第二个窗体上
- 在文本字段中输入新值
- 按下后退按钮
- 观察他们键入的值已替换表格视图中的原始文本
手动测试过程如下所示(这是一个动画的.gif-如果使用Medium应用程序,则可能需要在浏览器中打开此页面才能查看动画)。
如果我们可以自动化该过程,这样我们的QA测试人员就不必在每个版本之前都手动重复此过程,那不是很好吗? 这正是UI测试的目的所在-我们将逐步介绍如何使该测试过程自动化!
UI测试架构
在深入研究代码之前,了解Xcode UI Test框架的工作方式非常重要。 通过了解UI测试如何访问和操纵UI组件,您将能够使您的UI易于构建测试。
与单元测试 (使用源代码的单元测试一样),XCode使用XCTest来运行UI测试。 但是XCTest代码如何知道如何检查和操作在Storyboard和/或Swift代码中设计的UI?
为了在运行时访问您的UI,XCTest使用iOS Accessibility公开的元数据。 例如,这与使iOS能够向盲人和弱视用户读取屏幕的技术相同。 在运行时,XCTest遍历您的UI控件,查找可访问性属性(例如accessibilityIdentifier
和accessibilityLabel
以查找您希望XCTest在UI测试中进行点击,更改或检查的UI组件。
尽管无需在应用中进行任何可访问性元数据的准备就可以设计UI测试-并且您会在互联网上找到很多这样做的示例-您可以通过预先计划UI测试来保持UI测试的更好控制和可预测性,并在UI中准备辅助功能元数据。 同样,如果要将UI测试改造为现有应用程序,则应考虑将辅助功能元数据改造为该过程的一部分。
UI测试记录
Xcode的UI测试套件提供了一种开始实施UI测试的简便方法:“ 记录UI测试”按钮。
要开始记录UI测试:
- 在UI测试目标源.swift文件中创建一个新的UI测试功能(假设您在创建项目时创建了UI测试目标-或以后添加了它)
- 将编辑光标置于空白测试功能内
- 按下源代码编辑窗格下面的Record UI Test按钮
Xcode将使用调试设备(即模拟器)编译并运行应用程序。 然后,只需遍历模拟器(或其他调试设备)上的测试序列即可。 完成后,停止调试会话。 Xcode将创建一组命令,以在录制过程中重新创建UI体验。 对于上面概述的测试序列,将生成以下代码:
func testChangeTableRowText(){
让app = XCUIApplication()
app.tables [“ MyTable”]。staticTexts [“第四行”] .tap()
让newvalueTextField = app.textFields [“ newValue”]
newvalueTextField.tap()
设app2 = app
app2.buttons [“ shift”]。tap()
newvalueTextField.typeText(“一些新值”)
app.navigationBars [“ UITestingDemo.DetailView”]
.buttons [“返回”] .tap()
}
大! Xcode生成了重新运行我们手动执行的同一UI测试过程所需的所有命令。 这是我们测试设计生产率的福音,并且为我们提供了一个良好的开端。 但这还不是完美的,还不是可以进行生产的测试。 有一些不足之处:
- 有一些凌乱的方面,例如
let app2 = app
这行。 我们不会自己这样编写代码-在第1行创建的app
对象显然可以在整个测试功能中使用。 - 函数第2
staticTexts[“Fourth Row”]
对staticTexts[“Fourth Row”]
的引用假定UITableView
单元格的内容将始终相同。 如果不会呢? 在这种情况下,准备可访问性元数据可以帮助进行更可靠的测试。 我会尽快介绍。 - 自动生成的代码将导致测试运行,但是这里没有任何内容可以评估测试的结果是否成功。 Xcode无法创建测试的这一部分-我们必须自己做。
准备辅助功能元数据
在自动生成的代码的第2行中,Xcode插入了以下行:
app.tables["MyTable"].staticTexts["Fourth Row"].tap()
用英语来说,此命令的意思是:
在当前
UIView
的UITableView
对象的数组中,找到具有MyTable键的UITable
。 然后,搜索该UITable
所有UILabel
控件,并找到一个具有文本值“第四行”的UILabel
。 然后点击该UILabel
。
XCTest用于在此处查找UI元素的主要参考有两个:
- “第四行”
UILabel
-UITable的第4个UITableViewCell上显示的UILabel
文本值 - 带有“ MyTable”键的
UITableView
是吗? 钥匙从哪里来?
让我们考虑第二项。 在这种情况下,我之前已将文本“ MyTable”分配为第一个UIView
上的UITable
的accessibilityIdentifier
。 这是在该UIView's
UIViewController
的viewDidLoad()
函数中完成UIView's
,如下所示:
覆盖func viewDidLoad(){
super.viewDidLoad()
tableView.accessibilityIdentifier =“ MyTable”
}
每个UIView
都可以具有accessibilityIdentifier
以及其他Accessibility属性。 为了进行UI测试,您将对accessibilityIdentifier
和accessibiltyLabel
最为感兴趣。
当UIView
具有accessibilityIdentifier
或accessibilityLabel
,可以使用该字符串作为键在UI Test中对其进行查询。 例如,可以通过以下两种方式之一在UI测试中访问该表:
让tableView = app.tables。contains(.table,标识符:“ MyTable”)
让tableView = app.tables [“ MyTable”]
通过以这种方式使用可访问性元数据,您可以创建更强大的UI测试-一种不依赖控件中文本内容的测试。 而是可以通过您定义和控制的字典键值来访问控件。 但是您确实需要努力分配密钥才能使用它们!
注意:虽然可以使用
accessibilityIdentifier
或accessibilityLabel
来查询UIView
对象,但通常最好使用accessibiltyIdentifier
。accessibilityLabel
是iOS Accessibility用来访问盲人或弱视用户阅读的文本的属性,对于具有可更新文本属性的控件,在运行时可能会更改。
如何设置可访问性标识符
可以通过几种方法来设置基于UIView
的对象的accessibilityIdentifier。 最常见的情况如下:
使用Interface Builder身份检查器
某些UI元素支持在IB Identity Inspector中设置辅助功能属性。 例如,在我们测试解决方案的第一种形式上的UILabel
将其accessibilityIdentifier
直接在预定义的IB字段中设置为“ labelIdentifier” 。
使用用户定义的运行时属性
对于通常不会为盲人或弱视最终用户阅读的UI元素,Interface Builder将没有预定义的Accessibility属性字段。 但是,您仍然可以使用Identity Inspector上的“ 用户定义的运行时属性”字典编辑器在Interface Builder设计时添加它们。
在这种情况下,我已经将UITableView
的accessibilityIdentifier
从UIViewController
的viewDidLoad()
方法移到了Interface Builder故事板编辑器中。 最终的UI测试以完全相同的方式工作—但需要维护的代码更少。
使用代码
如前所述,每个基于UIView的类都具有可访问性属性,并且可以在运行时设置这些属性。
覆盖func viewDidLoad(){
super.viewDidLoad()
tableView.accessibilityIdentifier =“ MyTable”
}
这三种方法都具有相同的效果。 最佳选择取决于团队中的最佳实践。 一些人喜欢通过在Interface Builder中配置UI来减少代码,其他人则喜欢在代码中进行所有UI设计。 UI测试同样很好地支持这两种方案。
在测试期间检查UI元素
回想一下,我记录了测试步骤-但实际上我没有测试任何东西! 让我们通过添加实际测试来结束这项工作,并尽可能使用accessibilityIdentifier
属性。
搜索UIView元素
回想一下Xcode编写了以下语句,以使用其accessibilityIdentifier查找UITableView:
让tableView = app.tables [“ MyTable”]
这是最简洁的速记方法,但我想指出在视图层次结构中找到tableView的答案不只一个。
另一种方法是显式搜索accessibilityIdentifier:
让tableView = app.tables。contains(.table,标识符:“ MyTable”)
如果未分配accessibilityIdentifier,则可以使用以下代码获取顶级UIView中的第一个UITableView:
让tableView = app.tables.element(boundBy:0)
这不是很好,因为如果我们曾经在屏幕上添加第二个UITableView
,则如果恰巧检索到一个新的UITableView
作为第一个UITableView
,则UI测试可能会中断! 这就是我建议在设计UI测试时使用accessibilityIdentifiers
的原因。
如果我们知道屏幕上只有一个UITableView
,那么我们可以进一步缩短以前的技术:
让tableView = app.tables
同样,如果添加了第二个UITableView
则存在破坏UI测试的风险。 这将是一个更严重的中断,因为tables
属性将返回一个集合,而不是像视图层次结构中只有一个UITableView
时那样返回单个表。
最终测试脚本
我们已经介绍了创建测试,访问元素和操纵值的基础知识(Xcode在测试记录过程中向我们展示了这些知识),因此我们准备好进行总结。
我将其粘贴在最终测试功能的下方,然后在下方进行注释。
01:函数testChangeTableRowText(){
02:让app = XCUIApplication()
03:让tableView = app.tables [“ MyTable”]
04:XCTAssert(tableView.cells.count == 5)
05:
06:让单元格= tableView.cells。contains(.cell,标识符:“ 3”)
07:让cellLabelText = cell.staticTexts.element(boundBy:0).label
08:XCTAssertEqual(cellLabelText,“第四行”)
09:
10:cell.staticTexts.element(boundBy:0).tap()
11:
12://现在可以看到详细信息表单
13:
14:XCTAssertEqual(app.staticTexts [“ labelIdentifier”]。label,cellLabelText)
15:
16:让textField = app.otherElements.textFields [“ newValue”]
17:textField.tap()
18:textField.typeText(“某些新值”)
19:
20:XCTAssertEqual(textField.value as?String ??“”,“ Some new value”)
21:
22:app.navigationBars [“ UITestingDemo.DetailView”]。buttons [“ Back”]。tap()
23:
24://现在可以看到详细信息表单
25:
26:让tableView2 = app.tables。contains(.table,标识符:“ MyTable”)
27:让cell2 = tableView2.cells。contains(.cell,标识符:“ 3”)
28:让updatedText = cell2.staticTexts.element(boundBy:0).label
29:
30:XCTAssertEqual(updatedText,“一些新值”)
31:}
- 在第2至4行中,我们找到带有
accessibilityIdentifier
“ MyTable”的UITableView
,然后检查行数是否为五(5)。 请记住,每当XCTAssert
失败时,整个测试都会失败。 - 在第6行中,我们在
UITableView
搜索一个accessibilityIdentifier
等于“ 3”的UITableViewCell
。 此值是在UITableView
DataSource
委托的cellForRowAt
方法中设置的(有关详细信息,请查看GitHub中的代码) - 在第7行,我们在单元格中获得了第一个
UILabel
(此单元格只有一个标签)。 - 在第8行中,对照预期值检查了
UILabel
文本属性(这并不是此测试的真正要求,但我将其添加为进一步的示例)。 - 第10行将点击事件发送到单元内的
UILabel
。 这样做的效果是在单元上生成一个tap事件,然后触发对详细信息表单的segue(有关详细信息,请参见GitHub上的源代码) - 第14行找到带有
accessibilityIdentifier
“ labelIdentifier”的UILabel
(我们在之前的Interface Builder中进行了设置。加载表单时,它应该已经将UILabel
文本设置为在UITableView
点击的值。此XCAssetEqual
检查以确保已完成此操作。 - 第16–20行点击
UITextField
,然后输入新文本。 - 第22行点击了详细信息表单左上角的“返回”按钮,这将视图控制器从堆栈中弹出,返回到第一个表单。
- 第26-30行再次在第4个单元格中检索该值,并将新值与在明细表中键入的值进行比较。
注意:使用iOS模拟器创建键入字段的测试时,请确保断开模拟器中的硬件键盘。 当硬件键盘连接到模拟器时,
typeText
方法将失败。
下一步去哪里
至此,我们已经为应用程序的这一部分完成了一个完整的,健壮的UI测试!
由于我们尽可能使用了accessibilityIdentifier
属性,因此我们创建了一个测试,当使用新控件增强UI时,该测试不会轻易中断,并且该测试是可重复的,可自动化的,并且易于用于回归测试。
但是此测试可以进一步提高:
- 测试中我们仍然有一些静态数据值,例如“第四行” 。 通过重构此测试中的所有静态值假设,我们可以将其设置为与动态数据一起使用(例如,针对Web服务调用)
- 该测试仍然与使用Xcode的开发人员或质量检查工程师绑定在一起。 但是,通过其他一些工作,我们可以将这种类型的测试合并到由守护程序运行的全自动测试套件中。 在将来的博客文章中查找!