TABTestKit
TABTestKit 是一个笼罩在 XCUI/XCTest 之上的极富可读性、强类型封装,用于 iOS 自动化,它有助于降低易失性和降低大多数人的入门门槛。
TABTestKit 还使得首次能够可靠地自动化 iOS 的 生物识别功能,以及自动化 应用程序的深度链接,无需任何外部依赖!
func test_login() {
Scenario("Register for the app") {
Given(I: amNotRegistered)
When(I: registerForTheApp)
Then(I: see(the: loginScreen))
}
Scenario("Logging in successfully with biometrics") {
Given(I: see(the: loginScreen.logInButton))
And(the: deviceBiometricsAreEnabled)
And(the: state(of: loginScreen.loginButton, is: .enabled))
When(I: tap(loginScreen.logInButton))
And(I: successfullyAuthenticateBiometrics)
Then(I: see(the: profileScreen))
}
}
快速入门
第1步:创建代表您UI的屏幕
// ProfileScreen.swift
struct ProfileScreen: Screen {
let trait = Header(id: "Not Instagram") // The trait of a screen is what consistently and uniquely identifies it. It can be anything that conforms to `Element`
let usernameLabel = Label(id: "UsernameLabel") // IDs can either be the accessibilityLabel or accessibilityIdentifier of views in the app
let logOutButton = Button(id: "Log out")
}
let profileScreen = ProfileScreen() // Create an instance of your screen that is globally available
第2步:创建一个继承自TABTestCase的测试用例
// ProfileTests.swift
class ProfileTests: TABTestCase {
func test_loggingOut() {
}
}
第3步:编写描述您测试的步骤和场景
// ProfileTests.swift
class ProfileTests: TABTestCase {
func test_loggingOut() {
Scenario("Logging out") { // Scenarios group steps with a description, so you can have multiple Scenarios in a test function
Given(I: see(profileScreen)) // Steps can be either Given, When, Then, or And and can take any function which is executed automatically
And(the: value(of: profileScreen.usernameLabel, is: "My username")) // This `value(of)` function comes for free with TABTestCase!
When(I: tap(profileScreen.logOutButton))
Then(I: doNotSee(profileScreen))
}
}
}
第4步:创建更多屏幕、场景和步骤!
一旦开始定义更多的Screen
,您就可以在应用程序中执行更长的流程,并开始利用场景和步骤,以及利用元素
和上下文
。
为什么?
TABTestKit 使您能够用非常少的代码来自动化应用程序中的行为,同时通过利用强大的Swift特性,使测试变得非常易于阅读。
这意味着Swift自动化入门门槛大大降低,允许团队中的更多人参与到测试中。
此外,TABTestKit习惯于等待一些事情发生,而不是立即断言,这大大减少了测试中的不稳定性。
与XCTest不同,在XCTest中,每个XCUIElement
都可以进行点击、调整、输入或滚动操作,无论其是否可以调整等,而TABTestKit有特定的元素来表示常见的UIKit和AppKit元素,如Button
、Slider
和ScrollView
。
TABTestKit出厂时包含大多数常见元素,但也完全可定制,可以通过创建自定义元素,或者通过扩展现有元素来实现。
该框架旨在使编写自动化测试尽可能快速,同时也使编写错误、不稳定的自动化测试或犯错误变得更困难。
例如,TABTestKit引入了parent
元素的概念,这样你总是尝试在适当的父元素中引用正确的元素。这当通过标签匹配按钮时非常重要,可能会有多个视图具有相同的文本(例如屏幕和弹窗中的“确定”)。
TABTestKit还提供了一种BDD风格的测试编写方法,这意味着你可以编写步骤,分组在场景中,这意味着即使是几乎没有编程经验的人也可以参与并编写自动化测试,因为它们读起来(大部分)就像英语。
你可以在项目中混合和匹配你想要的任何TABTestKit部分,因此你不会受到框架的限制。
请查看示例应用程序,了解如何使用TABTestKit进行自动化测试,几乎不需要编写任何自定义代码。
用法
TABTestKit有几个组件,你可以挑选使用(尽管我们认为你应该使用所有组件)
TABTestCase
TABTestCase
是您应当让你的所有测试用例继承的基本测试用例类
class MyTestCase: TABTestCase {
func test_something() {
// Your tests here
}
}
通过继承TABTestCase
,您可以免费获得大量的设置行为,比如设备生物识别已注销以便为下一个测试做好准备,以及一个作为启动环境变量的UUID,您可以使用它来唯一地识别并行测试中的您的应用实例。
这对模拟服务器很有用,这样您可以根据每个运行的实例调整模拟服务器的响应。
注意:您可以在应用程序中随时使用
ProcessInfo.process.environment["TABTestKit.UUID"]
查询UUID。
TABTestCase
在每个测试的开始和结束时自动启动和终止您的应用。您可以延迟启动或终止以提供额外的设置或拆除(例如重置模拟服务器),通过覆盖 preLaunchSetup
或 preTerminationTearDown
实现。
class MyTestCase: TABTestCase {
override func preLaunchSetup(_ launch: @escaping () -> Void) {
// Some pre-launch setup code
launch()
}
func test_something() {
// Your tests here, that will be executed after `launch()` is called above
}
}
注意:如果您覆盖
preLaunchSetup
或preTerminationTearDown
并需要在启动或终止应用之前执行一些异步工作,您将需要通过使用XCTWaiter
强制测试等待任务完成。
步骤和场景
将测试编写为步骤和场景是使其具有良好可读性和可访问性的重要部分,让每个人都能理解。
步骤和场景是 TABTestKit 独特之处的一部分,并且由于框架非常灵活,如果您喜欢,可以选择仅使用步骤和场景,而无需使用 TABTestKit 的其余部分。
步骤
步骤是运行在您的测试中的单个函数,如下所示
func test_serverErrorLoggingIn() {
Given(the: mockServer(is: .failing)))
When(I: logIn))
Then(I: seeAnError))
}
// Somewhere else in code:
enum MockServerState {
case failing
case succeeding
}
func mockServer(is state: MockServerState) {
// Code to set the mock server state
}
func logIn() {
// Code to try and log in
}
func seeAnError() {
// Code to assert the error screen is showing
}
上面步骤的语法可能会让 Swift 开发者觉得有点奇怪。Given
/When
/Then
实际上只是 Step
结构体的一个别名。
Step
结构体具有一个 init
,它接受任意函数,该函数在步骤的正确时间自动执行。
真正好的一面是,您可以 通过引用传递函数,这本质上意味着您可以省略步函数中的括号,这使得您的代码读起来更像一个正常的句子
When(I: logIn)) // Instead of When(I: logIn()))
场景
场景通过带有描述的逻辑组包装步骤,这样您可以快速阅读测试函数以找到您正在寻找的代码区域
func test_serverErrorLoggingIn() {
Scenario("Attempting log in when the server is failing") {
Given(the: mockServer(is: .failing)))
When(I: logIn))
Then(I: seeAnError))
}
Scenario("Dismissing the error screen") {
Given(I: seeAnError))
When(I: dismissTheError))
Then(I: seeTheLoginScreen))
}
}
生物识别
另一个 TABTestKit 的独家功能是它使在模拟器中自动化 iOS 生物识别变得可能(并且非常容易!)
注意:如果您在使用 Swift Package Manager,您需要
导入
Biometrics
模块才能直接使用Biometrics
类。直到问题118被关闭
启用和禁用设备生物识别
Biometrics.enrolled()
Biometrics.unenrolled()
注意:“注册”一词来自模拟器菜单选项,用于启用或禁用生物识别。
模拟成功或失败的面部识别或指纹识别
Biometrics.successfulAuthentication()
Biometrics.unsuccessfulAuthentication()
这适用于面部识别和指纹识别模拟器,但不会在物理设备上工作。
注意:示例应用中有一个示例,展示了如何在测试期间成功授予面部识别权限。
屏幕和元素
屏幕
TABTestKit
中的Screen
代表您的应用程序中的一个屏幕(即UIViewController),并且是为了自动化测试清晰地定义屏幕结构的一种非常好的方法。
它有点像SwiftUI,您描述屏幕由哪些元素组成。
要定义屏幕,只需创建一个符合Screen
的struct
。
struct ProfileScreen: Screen {
let trait = Header(id: "My Profile")
}
Screen
有一个必须实现的属性,即其trait
。一个trait
可以是任何可以一致且唯一地标识屏幕的Element,并在使用context进行测试时用于等待它出现在屏幕上。
元素
Element
(元素)在 TABTestKit 中代表屏幕上所有不同的元素,如 View
、Header
、Slider
、Alert
等等。
与 XCTest 不同,其中的所有 XCUIElement
对象都可以进行点击、滑动和输入,TABTestKit 中的 Element
会声明它是否可以滑动(即 ScrollView
、TextView
、TableView
等),是否具有值(即 Header
、Label
、Switch
等)等等。
您可以使用元素来描述您的屏幕
struct ProfileScreen: Screen {
let trait = Header(id: "My Profile")
let logOutButton = Button(id: "Log out")
let nameLabel = Label(id: "Kane Cheshire")
let changePasswordField = SecureTextField(id: "ChangePasswordField")
}
一旦您创建了您的屏幕,您就可以在测试中使用它来与元素交互
// If using Swift 5.4 or below
let profileScreen = ProfileScreen()
profileScreen.await() // Makes sure the screen is visible before going any further by waiting for its trait
profileScreen.logOutButton.tap() // You can't call tap on an element that isn't `Tappable`, but `Button` is!
// If using Swift 5.5
let profileScreen = ProfileScreen()
profileScreen.waitFor() // Makes sure the screen is visible before going any further by waiting for its trait
profileScreen.logOutButton.tap() // You can't call tap on an element that isn't `Tappable`, but `Button` is!
所有元素都符合 Element
协议,该协议确保每个元素都有一个父元素(默认为 App
),基础的 type
(类型)、index
(索引,默认为 0
),并有一个可选的 ID。
TABTestKit 使用这些信息在您想要使用时自动查询 XCTest 的正确 XCUIElement
,将父元素作为根。所有这些操作都自动完成,因此您无需学习 XCUIElementQuery
即可开始。
父元素
因为所有元素都必须有一个父元素,这使得在屏幕上有多个匹配的情况下确保引用正确的元素变得非常强大。
一个例子是当查找警告中的按钮。使用 XCTest,只需查找以按钮标签作为其 ID 的第一个匹配的按钮,例如 "OK",是非常容易和常见的。
XCUIApplication().buttons["OK"].firstMatch
屏幕上可能还存在带 "OK" 的其他按钮,所以正确的按钮可能不会匹配,或者可能是带有 OK 按钮的错误警告。
TABTestKit 使得引用错误的按钮变得更为困难,因为您使用 Alert
元素来与之交互其中的按钮
let alert = Alert(id: "An error occurred!")
alert.actionButton(withID: "OK").tap()
在上面的测试中,OK 按钮不会引用除了警告中的按钮之外的其他任何内容,因为 Alert
是作为 parent
向您提供 Button
。
您也可以自己设置元素的父元素
let customView = View(id: "CustomView") // By default, the `App` is the parent of `Element`s
let button = Button(id: "OK", parent: customView) // Now only OK buttons within customView will be matched
TABTestKit 会自动为您构建底层的 XCUIElementQuery
(尽管如果您想,您也可以自己完成),所以您需要做的就是描述您的 UI。
使用 Element
和其他协议(如 Tappable),在 上下文 中使用时,还具有更多好处,您可以在 InteractionContext
文档中了解更多信息。
创建您自己的元素
TABTestKit 提供了许多常用的元素,您可以使用它们来表示和与您的界面交互,但您也可以创建自定义元素,如果预定义的元素都不适合您。
要创建自己的,您可以创建一个符合 Element
协议的结构。
扩展现有元素
默认情况下, Element
中的元素在 TABTestKit 不支持任何交互(除非您使用 underlyingXCUIElement
)。
支持交互的元素通过符合其他协议来声明,如 Button
同时符合 Element
和 Tappable
。
如果您需要任何元素的额外行为,可以创建一个扩展并添加更多功能,例如通过符合另一个协议或创建新的属性和函数。
extension Button {
func doubleTap() {
underlyingXCUIElement.doubleTap()
}
}
如果您认为应该对元素进行某些行为或改进,鼓励您在存储库中提出问题或创建拉取请求,以便其他人可以从这些改进中受益。
预定义元素
视图
View
代表应用中的一个通用视图
let view = View(id: "MyCustomView")
通常,您会使用 View
作为 Screen
的特征,通过在 的视图中设置标识符来设置。
func viewDidLoad() {
super.viewDidLoad()
view.accessibilityIdentifier = "MyCustomView"
}
或者,您可能将 View
作为容器视图使用(特别是对于 SegmentedControl
)。
标题
Header
表示应用中的标题或表头
let header = Header(id: "My header text")
let header = Header(id: "MyHeaderIdentifier")
您可以使用Header
的文本或自定义ID作为标识符。
为了使Header
,您的UIKit视图必须设置header属性
let myHeaderLabel = UILabel()
myHeaderLabel.accessibilityTraits = .header
这样做不仅可以使您的UI更具语义感,还意味着可访问性用户也可以更好地导航。双赢!
一些UIKit视图已经默认设置了此属性,例如UITableView
中的标题或UINavigationBar
中的标题。
标签
Label
表示应用中的通用文本标签
let label = Label(id: "My label text")
let label = Label(id: "MyLabelIdentifier")
与Header
类似,您可以使用Label
的文本或自定义ID作为标识符。
为了使Label
,您的UIKit视图必须设置.staticText
属性
let myLabel = UILabel()
myLabel.accessibilityTraits = .staticText
在大多数情况下,这项工作会自动完成,除非您进行了自定义。
按钮
let button = Button(id: "My button text")
let button = Button(id: "MyButtonIdentifier")
与和
类似,您可以使用
的文本或自定义ID作为标识符。
因为Button符合规则,您可以点击它
button.tap()
为了使,您的UIKit视图必须设置
属性
let myButton = UIButton()
myButton.accessibilityTraits = .button
在大多数情况下,这项工作会自动完成,除非您进行了自定义,您可能需要在具有点击手势识别器的自定义视图中自己设置这个属性,例如。
滚动视图
表示应用中的滚动视图
let scrollView = ScrollView()
let scrollView = ScrollView(id: "MyScrollView")
由于通常一次只有屏幕上有一个滚动视图,您不需要提供ID以创建>TABTestKit中的滚动视图,除非需要。
由于符合
规则,您可以滚动它
scrollView.scroll(.downwards)
表
表
表示应用中的表视图
let table = Table()
let table = Table(id: "MyTable")
和ScrollView
一样,由于屏幕上通常只显示一个表视图,所以创建表时不需要提供ID。
由于表
符合滚动
,您可以对它进行滚动
table.scroll(.upwards)
由于表
还符合包含单元格
,您可以使用索引检索单元格数量
let allCellsCount = table.numberOfCells()
let allCellsCountMatchingID = table.numberOfCells(matchingID: "MyTableCellID")
以及检索一个单元格
let firstCell = table.cell(index: 0)
let lastCell = table.cell(index: allCellsCount - 1)
let firstCellWithID = table.cell(matchingID: "MyTableCellID", index: 0)
let lastCellWithID = table.cell(matchingID: "MyTableCellID", index: allCellsCountMatchingID - 1)
CollectionView
CollectionView
表示应用中的集合视图
let collectionView = CollectionView()
let collectionView = CollectionView(id: "MyCollectionView")
和ScrollView
以及表
一样,由于屏幕上通常只显示一个CollectionView,所以创建它时不需要提供ID。
由于CollectionView
符合滚动
,您可以对其进行滚动
collectionView.scroll(.left)
由于CollectionView
也符合包含单元格
,您可以使用索引检索单元格数量
let allCellsCount = collectionView.numberOfCells()
let allCellsCountMatchingID = collectionView.numberOfCells(matchingID: "MyCollectionViewCellID")
以及检索一个单元格
let firstCell = collectionView.cell(index: 0)
let lastCell = collectionView.cell(index: allCellsCount - 1)
let firstCellWithID = collectionView.cell(matchingID: "MyCollectionViewCellID", index: 0)
let lastCellWithID = collectionView.cell(matchingID: "MyCollectionViewCellID", index: allCellsCountMatchingID - 1)
单元格
单元格
表示应用中的单元格。单元格通常位于表格或集合视图中。
尽管您可以使用表
或CollectionView
(或其他任何Element
)作为父元素来创建Cell
let table = Table()
let cell = Cell(index: 0, parent: table)
但最好是向表
或CollectionView
请求单元格,这将自动拥有正确的父元素
let tableCell = table.cell(index: 0)
let collectionViewCell = collectionView.cell(index: 0)
由于单元格
符合可点击
,您可以点击它
table.cell(index: 0).tap()
TextField
TextField
表示应用中的普通文本字段
let textField = TextField(id: "MyTextField")
注意:应使用
SecureTextField
来表示具有安全输入的UITextFields。
由于TextField
符合可编辑
,您可以在其中输入文本或从其中删除文本
textField.type("Hello!")
textField.delete(numberOfCharacters: 6)
由于TextField
还符合可点击
,您可以点击它
textField.tap()
并且,由于TextField
也遵循了ValueRepresentable
的规范,因此您也可以检索到String
值
XCTAssertEqual(textField.value, "Hello!")
SecureTextField
SecureTextField
表示应用中的安全文本字段(即密码字段)
let secureTextField = SecureTextField(id: "MySecureTextField")
由于SecureTextField
遵循了Editable
规范,因此您可以在其中输入文本或删除文本
secureTextField.type("Password1")
secureTextField.delete(numberOfCharacters: 6)
由于SecureTextField
也遵循了Tappable
规范,因此您可以点击它
secureTextField.tap()
并且,由于SecureTextField
也遵循了ValueRepresentable
规范,因此您也可以检索到String
值
XCTAssertEqual(secureTextField.value, "•••••••••") // Not sure how useful this is but you can assert it if you want 😂
TextView
TextView
表示应用中的文本视图
let textView = TextView(id: "MyTextView")
由于TextView
遵循了Editable
规范,因此您可以向其中输入文本或删除文本
textView.type("Lorem Ipsum")
textView.delete(numberOfCharacters: 12)
由于TextView
也遵循了Tappable
规范,因此您可以点击它
textView.tap()
并且,由于TextView
也遵循了ValueRepresentable
规范,因此您也可以检索到String
值
XCTAssertEqual(textView.value, "Lorem Ipsum")
并且,由于TextView
也遵循了Scrollable
规范,因此您也可以滚动它
textView.scroll(.downwards)
Keyboard
Keyboard
表示应用中的软件键盘。由于屏幕上通常只存在一个软件键盘,因此存在一个全局实例(在测试中可用)keyboard
,您可以在任何位置使用。
这对于许多事情都很有用,比如检查键盘是否可见
// If using Swift 5.4 or below
keyboard.await(.visible)
// If using Swift 5.5
keyboard.waitFor(.visible)
检查当前软件键盘是否为预期的类型
XCTAssertEqual(keyboard.keyboardType, .regular)
XCTAssertEqual(keyboard.keyboardType, .emailAddress)
或者甚至用它来确保在滚动时避开键盘
let scrollView = ScrollView()
scrollView.scroll(.from(keyboard.topCoordinate, to: .top))
您还可以使用Keyboard
元素来生成键,这些键自动将键盘设置为父级
let aKey = keyboard.key("a")
由于Keyboard.Key
遵循了Tappable
规范,因此您可以点击它们
aKey.tap()
您可以使用它们与InteractionContext
Given(I: tap(aKey))
此外,您还可以使用Keyboard
与KeyboardContext
NavBar
NavBar
表示您应用中的一个导航栏
let navBar = NavBar()
let navBar = NavBar(id: "MyNavBar")
由于屏幕上通常一次只有一个导航栏,因此不需要提供ID,但您可以提供。
NavBar
可以提供其 Header
,您可以使用它来断言值
XCTAssertEqual(navBar.header.value, "Screen Title")
TabBar
TabBar
表示您应用中的一个标签栏
let tabBar = TabBar()
let tabBar = TabBar(id: "MyTabBar")
由于屏幕上通常一次只有一个标签栏,因此不需要提供ID,但您可以提供。
TabBar
可以提供其 Button
按钮
tabBar.button(withID: "Tab 1").tap()
与其他 Button
类似,按钮的ID可以是按钮标题,或者您设置的作为 accessibilityIdentifier
的自定义ID。
Alert
Alert
表示应用中的提示(有时也称为弹出窗口或对话框)
let alert = Alert(id: "My alert title")
let alert = Alert(id: "My alert title", dismissButtonID: "OK")
通常,提示的ID是标题。如果您的提示不是通过“取消”来关闭的,您也可以提供可选的 dismissButtonID
。
Alert
根据ID提供操作按钮,因此您可以确保您在屏幕上引用正确的按钮
alert.actionButton(withID: "Delete").tap()
由于 Alert
符合 Dismissable
,您可以将其关闭
alert.dismiss()
注意:
Alert
使用dismissButtonID
来关闭自己,默认为“取消”。
Sheet
Sheet
表示应用中的一个活动表单
let sheet = Sheet(id: "My sheet title")
let sheet = Sheet(id: "My sheet title", dismissButtonID: "OK")
通常,表单的ID是其标题。如果您的表单不是通过“取消”来关闭的,您也可以提供可选的 dismissButtonID
。
Sheet
根据ID提供操作按钮,因此您可以确保您在屏幕上引用正确的按钮
sheet.actionButton(withID: "Delete").tap()
由于 Sheet
符合 Dismissable
,您可以将其关闭
sheet.dismiss()
注意:
Sheet
使用dismissButtonID
来关闭自己,默认为“取消”。
Activity Sheet
ActivitySheet
代表应用中的 UIActivityViewController
(分享选项卡)。
let shareSheet = ActivitySheet()
let shareSheet = ActivitySheet(parent: Safari())
由于 ActivitySheet
符合 Dismissable
协议,您可以直接与 NavigationContext
一起使用。
Given(I: dismiss(shareSheet))
切换
Switch
代表应用中的开关(有时称为切换器)。
let toggle = Switch(id: "MySwitch")
由于 Switch
符合 ValueRepresentable
协议,您可以获取当前状态。
XCTAssertEqual(toggle.value, .on)
XCTAssertEqual(toggle.value, .off)
由于 Switch
还符合 Adjustable
协议,您可以将状态设置为 开启
或 关闭
。
toggle.adjust(to: .on)
toggle.adjust(to: .off)
由于 Switch
还符合 Tappable
协议,您可以轻触它。
toggle.tap()
注意:如果值已经是您正在尝试设置的值,TABTestKit 将会失败测试。
滑动条
Slider
代表应用中的滑动条。
let slider = Slider(id: "MySlider")
由于 Slider
符合 ValueRepresentable
协议,您可以获取当前的 CGFloat
值。
XCTAssertEqual(slider.value, 0.5)
由于 Slider
还符合 Adjustable
协议,您可以设置当前的 CGFloat
值。
slider.adjust(to: 1)
注意:如果值已经是您正在尝试设置的值,TABTestKit 将会失败测试。
步进器
Stepper
代表应用中的步进器。
let stepper = Stepper(id: "MyStepper")
您可以通过在 Stepper
中与 incrementButton
和 decrementButton
交互来进行操作。
stepper.incrementButton.tap()
stepper.decrementButton.tap()
不幸的是,步进器底层的 XCUIElement
并不提供当前值,因此您将需要使用其他元素(如标签)来断言值。
但是,您可以对按钮的状态进行断言,例如检查按钮是否启用。
// If using Swift 5.4 or below
stepper.decrementButton.await(not: .enabled, timeout: 1) // Waits a max of 1 second for the button to be disabled
// If using Swift 5.5
stepper.decrementButton.waitFor(not: .enabled, timeout: 1) // Waits a max of 1 second for the button to be disabled
您可以在 Element
的文档中了解更多关于 await(not:)
/waitFor(not:)
以及其他 Element
方法的详细信息。
分段控件
SegmentedControl
代表应用中的分段控件。
let segmentedControl = SegmentedControl()
不幸的是,UIKit 似乎不支持在 UISegmentedControl
上设置自定义的 accessibilityIdentifier 或 accessibilityLabel,因此您不能像其他元素一样通过 ID 来引用它。
您可以通过在一个容器 UIView
中包装您的 UISegmentedControl
并给该容器视图一个标识符来解决此问题。
let segmentedControl = UISegmentedControl()
let containerView = UIView()
containerView.accessibilityIdentifier = "MySegmentedControl"
containerView.addSubview(segmentedControl)
然后您可以在测试中以该容器视图作为父视图来引用它。
let segmentedControl = SegmentedControl(parent: View(id: "MySegmentedControl"))
或者,您可以通过索引引用分段控件。
let segmentedControl = SegmentedControl(index: 0)
let segmentedControl = SegmentedControl(index: 1)
SegmentedControl
会根据 ID 提供分段按钮,因此您可以确保您正在引用屏幕上的正确按钮。
segmentedControl.button(withID: "Segment 2").tap()
选择器
Picker
代表了界面中的选择器
let picker = Picker(id: "MyPicker")
您不是直接与选择器交互,而是交互选择器内的轮盘。要交互轮盘,首先请求选择器获取轮盘
let wheel = picker.wheel(0)
由于 Picker
的 Wheel
符合 ValueRepresentable
,您可以获取字符串值
XCTAssertEqual(wheel.value, "The value")
由于 Picker
的 Wheel
也符合 Adjustable
,您可以将值调整为另一个 String
wheel.adjust(to: "New value")
日期选择器
DatePicker
代表界面中的日期选择器
let datePicker = DatePicker(id: "MyPicker")
在 iOS 13 和更低版本。您不是直接与选择器交互,而是交互选择器内的轮盘。要交互轮盘,首先请求选择器获取轮盘
let wheel = datePicker.wheel(0)
由于 DatePicker
的 Wheel
符合 ValueRepresentable
,您可以获取字符串值
XCTAssertEqual(wheel.value, "The value")
由于 DatePicker
的 Wheel
也符合 Adjustable
,您可以将值调整为另一个 String
wheel.adjust(to: "New value")
页码指示器
PageIndicator
代表界面中的页码控制或页码指示器
let pageIndicator = PageIndicator(id: "MyPageIndicator")
由于 PageIndicator
符合 ValueRepresentable
,您可以得到字符串值
XCTAssertEqual(pageIndicator.value, "page 1 of 3")
网页视图
WebView
代表界面中的网页视图
let webView = WebView(id: "MyWebView")
由于 WebView
符合 Scrollable
,您可以滚动它
webView.scroll(.left)
图像
图像
代表应用程序中的图像或图像视图
let image = Image(id: "ExampleImage")
由于 图像
与 ValueRepresentable
兼容,您可以获取标签(字符串)值
XCTAssertEqual(image.value, "image of random object")
图标
图标
通常表示主屏幕上的应用程序图标,但也可以表示XCUI视为图标的任何内容。
预定义屏幕
TABTestKit 随带一些有用的预定义屏幕,您可以在测试中使用。
SystemPreferencesRootScreen
系统设置应用的根目录。您可以使用它导航到 SystemPreferencesGeneralScreen
systemPreferencesRootScreen.generalCell.tap()
注意:您不需要创建
SystemPreferencesRootScreen
或其他预定义屏幕的实例,因为它们已经被创建,并且可以在测试的任何位置全局访问。
SystemPreferencesGeneralScreen
系统设置中的通用屏幕。您可以使用它导航到 SystemPreferencesResetScreen
systemPreferencesGeneralScreen.resetCell.tap()
系统设置重置屏幕
系统设置中的重置屏幕。您可以用它来重置隐私和权限提示,例如面部识别权限
systemPreferencesResetScreen.resetCell.tap()
confirmResetSheet.actionButton(withID: "Reset Warnings").tap()
上下文
上下文是共享代码的一种方式,可以在测试用例之间重用,而无需使所有功能都全局可用,或无需有复杂的继承。
上下文只是一个具有预实现函数的协议,因此每当测试用例符合该协议时,所有这些函数都将可用。
TABTestKit带有多种预定义的上下文,其中TABTestCase
已经符合规范,因此通过继承TABTestCase,您将自动访问到与步骤和场景以及元素
和其他协议完美配合的大量辅助函数
通常,您甚至不需要自己编写任何额外的函数,这将使您快速地为项目进行自动化变得容易。
导航上下文
NavigationContext
是TABTestCase
已经符合的预定义上下文之一,这意味着您的测试用例可以预先使用其中的函数。
NavigationContext
为在应用程序中导航提供辅助函数
断言屏幕和元素是否可见
使用NavigationContext
,您可以断言任何屏幕
或元素
是否可见
see(myScreen)
doNotSee(myScreen)
see(myScreen.button)
doNotSee(myScreen.button)
Given(I: see(myScreen))
Given(I: doNotSee(myScreen))
Given(I: see(myScreen.button))
Given(I: doNotSee(myScreen.button))
完成任务和删除
使用NavigationContext
,您可以完成并删除所有符合
Completable
Dismissable
complete(myScreen)
dismiss(myScreen)
dismiss(myScreen.alert)
Given(I: complete(myScreen))
Given(I: dismiss(myScreen))
Given(I: dismiss(myScreen.alert))
您也可以将任意数量的Completable
或Dismissable
传递给函数,这让您能够轻松地为应用建立流程
complete(nameScreen, birthDateScreen, usernameScreen)
打开网址和深度链接
在TABTestKit中使用NavigationContext
,您可以自动化和测试应用中的深度链接,还可以在Safari中打开网站
let deepLink = URL(string: "my-app://deep-link")!
open(deepLink)
let appleSite = URL(string: "https://apple.com")!
open(appleSite)
Given(I: open(deepLink))
Given(I: open(appleSite))
需要注意的是,为此,TABTestKit将自身启动测试运行器,并使用测试运行器打开网址。
尽管TABTestKit将导航到主屏幕并找到测试运行器应用以启动它,但决定在打开网址之前是保持应用当前状态还是首先终止应用则是您的责任。
这让您可以自由地测试您的应用如何处理来自多个状态的深度链接。
您可以使用
AppContext
交互上下文
InteractionContext
是一个预定义上下文,TABTestCase
已经符合,这意味着您的测试用例已经可以使用其中的函数。
InteractionContext
为与应用中的元素交互提供了辅助函数
点击元素
符合Tappable
的一切都可以使用InteractionContext
来点击
tap(myScreen.button)
tap(myScreen.textField)
doubleTap(myScreen.imageView)
twoFingerTap(myScreen.someView)
longPress(myScreen.cell)
Given(I: tap(myScreen.button))
Given(I: doubleTap(myScreen.imageView))
Given(I: twoFingerTap(myScreen.someView))
Given(I: longPress(myScreen.cell))
在元素中输入
任何符合Editable
规范的内容都可以使用InteractionContext
进行输入。
type("Hello", into: myScreen.textField)
delete(5, charactersFrom: myScreen.textField)
Given(I: type("Hello", into: myScreen.textField))
Given(I: delete(5, charactersFrom: myScreen.textField))
断言元素的state状态
使用InteractionContext
可以断言任意Element
的state状态。
state(of: myScreen.button, is: .enabled)
state(of: myScreen.title, isNot: .hittable)
Given(the: state(of: myScreen.button, is: .enabled))
Given(the: state(of: myScreen.title, isNot: .hittable))
TABTestKit底层在断言期望的state状态时,为了避免稳定性问题,会短暂等待,而不是立即断言,这是自动化测试中的常见错误。
也可以传入多个状态进行断言。
state(of: myScreen.button, is: .enabled, .visible)
滚动元素
任何符合Scrollable
规范的内容都可以使用InteractionContext
进行滚动,直到某个元素达到特定状态。
scroll(myScreen.scrollView, .downwards, until: myScreen.textField, is: .visible)
Given(I: scroll(myScreen.scrollView, .downwards, until: myScreen.textField, is: .visible))
也可以滚动到元素处于多个状态,这在滚动重复使用行的视图(如表格和集合)时特别有用,最后一个单元格可能不存在,因此可以滚动到它存在,然后继续滚动到它可见。
scroll(myScreen.tableView, .downwards, until: myScreen.lastCell, is: .exists, .visible)
除了滚动到元素处于某些状态之外,还可以滚动到元素不处于某些状态。
scroll(myScreen.tableView, .downwards, until: myScreen.firstCell, isNot: .visible)
断言和调整值
任何符合ValueRepresentable
规范的元素都可以使用InteractionContext
进行值断言。
value(of: myScreen.header, is: "My header text")
Given(the: value(of: myScreen.header, is: "My header text"))
为了避免稳定性问题,TABTestKit底层会在断言期望的值时,短暂等待,而不是立即断言,这是自动化测试中的常见错误。
任何符合Adjustable
规范的元素都可以使用InteractionContext
调整其值。
adjust(myScreen.toggle, to: .on)
adjust(myScreen.slider, to: 0.5)
Given(I: adjust(myScreen.toggle, to: .on))
Given(I: adjust(myScreen.slider, to: 0.5))
AppContext
AppContext
是一个预定义的上下文,TABTestCase
已经符合它的规范,这意味着您的测试用例已经可以使用其中的函数。
AppContext
提供了控制 App 的辅助函数。
将应用置于后台和前台
您可以使用 AppContext
将应用置于后台和前台。
backgroundTheApp()
foregroundTheApp()
Given(I: backgroundTheApp) // No need for `()` in Steps
Given(I: foregroundTheApp)
终止和启动应用
您可以使用 AppContext
终止、将应用置于后台以及重新启动应用。
terminateTheApp()
launchTheApp(clean: true)
relaunchTheApp()
Given(I: terminateTheApp) // No need for `()` in Steps
Given(I: launchTheApp(clean: true))
Given(I: relaunchTheApp)
注意:干净地启动应用需要在您的应用中进行一些工作。有关更多信息,请参阅 App。
AlertContext
AlertContext
是一个预定义的上下文,TABTestCase
已经符合它的规范,这意味着您的测试用例已经可以使用其中的函数。
AlertContext
提供了与 Alert 交互的辅助函数。
tap("OK", in: myScreen.alert)
Given(I: tap("OK", in: myScreen.alert))
message(in: myScreen.alert, is: "Alert message")
Then(the: message(in: myScreen.alert, is: "Alert message"))
SheetContext
SheetContext
是一个预定义的上下文,TABTestCase
已经符合它的规范,这意味着您的测试用例已经可以使用其中的函数。
SheetContext
提供了与 Sheet 交互的辅助函数。
tap("Delete", in: myScreen.sheet)
Given(I: tap("Delete", in: myScreen.sheet))
键盘上下文
键盘上下文
是一个预定义的上下文,TABTestCase
已经符合,这意味着您的测试用例已经可以使用其中的函数。
键盘上下文
提供了用于与Keyboard
交互的帮助函数。
keyboardType(is: .twitter)
Given(the: keyboardType(is: .twitter))
生物识别上下文
生物识别上下文
是一个预定义的上下文,TABTestCase
已经符合,这意味着您的测试用例已经可以使用其中的函数。
生物识别上下文
为测试中的生物识别提供了帮助函数。
注意:生物识别模拟仅在模拟器中起作用,不能在真实设备上运行
启用和禁用生物识别
生物识别上下文
允许您轻松控制模拟器上是否启用设备生物识别
deviceBiometricsAreEnabled()
deviceBiometricsAreDisabled()
Given(deviceBiometricsAreEnabled) // No need for `()` in Steps
Given(deviceBiometricsAreDisabled)
模拟成功和失败的认证
生物识别上下文
允许您轻松模拟成功或失败的认证。这适用于面部识别和触摸ID。
successfullyAuthenticateBiometrics()
failToAuthenticateBiometrics()
Given(I: successfullyAuthenticateBiometrics) // No need for `()` in Steps
Given(I: failToAuthenticateBiometrics)
系统偏好设置上下文
系统偏好设置上下文
是一个预定义的上下文,TABTestCase
已经符合,这意味着您的测试用例已经可以使用其中的函数。
系统偏好设置上下文
提供了用于与系统偏好设置交互的帮助函数。
开启和终止系统设置
openSystemPreferences()
terminateSystemPreferences()
Given(I: openSystemPreferences) // No need for `()` in Steps
Given(I: terminateSystemPreferences)
TabBarContext
TabBarContext
是一个预定义的环境,TABTestCase
已经符合它的规范,这意味着您的测试用例已经可以使用其中的函数。
TabBarContext
为 TabBar 中 tab 的计数提供了辅助函数。
func numberOfTabs(in tabBar: TabBar, is count: Int)
And(the: numberOfTabs(in: tabBarScreen.tabBar, is: 4))
重置所有隐私提示
SystemPreferencesContext
提供了一种方法,可以在模拟器中重置所有隐私提示,而无需删除和重新安装应用
resetAllPrivacyPrompts()
Given(I: resetAllPrivacyPrompts) // No need for `()` in Steps
例如,您可以在您的 preLaunchSetup
调用 resetAllPrivacyPrompts()
。
协议
TABTestKit 在幕后由协议驱动。除非您正在创建自己的元素,否则您可能只需要自己实现一些,例如 Screen
、Completable
和 Dismissable
。
屏幕
您使用 Screen
来描述应用中的特定屏幕。
每个 Screen
必须有一个特质来标识它,此特质可以是符合 Element
的任何事物。
struct MyScreen: Screen {
let trait = View(id: "MyViewControllerView")
}
通常,您的屏幕的特质会是 View
或 Header
,但它可以是任何可以持续标识您屏幕的事物。
trait
由 TABTestKit 在 NavigationContext
中用于断言屏幕当前是否在屏幕上。
可滚动屏幕
可滚动屏幕
类似于Screen
,但为了符合规范,您必须设置一个表示元素
的trait
,该元素也符合Scrollable
规范
struct MyScrollableScreen: ScrollableScreen {
let trait = ScrollView() // Since ScrollView conforms to `Scrollable` it can be used as the trait.
}
通过符合可滚动屏幕
规范,您的屏幕将自动具有可滚动
性,您可以直接使用它进行滚动或将它传递给接受Scrollable
参数的InteractionContext
函数
collectionViewScreen.scroll(.downwards)
Given(I: scroll(myScrollableScreen, .downwards, until: myScrollableScreen.textField, is: .visible))
可完成
通常情况下,您会让您的屏幕符合可完成
规范
extension MyScreen: Completable {
func complete() {
nextButton.tap()
}
}
complete
函数应知道如何自行完成,对于屏幕来说,就是通过其主流程顺利通过。
您可以使任何内容都符合可完成
规范,并与NavigationContext
一起使用
可忽略
通常情况下,您会让您的屏幕符合可忽略
规范
extension MyScreen: Dismissable {
func dismiss() {
backButton.tap()
}
}
dismiss
函数应知道如何自行忽略,对于屏幕来说,通常是点击返回、完成或关闭按钮
您可以使任何内容都符合可忽略
规范,并与NavigationContext
一起使用,就像Alert
一样
元素
元素
是TABTestKit的骨干,实际上是XCUIElement
的替代品(技术上讲,元素
封装了XCUIElement
)。
每个元素
都必须提供
- 可选的
id
- 元素的
父元素
(通常是应用程序) - 元素的
索引
(在存在多个匹配项或单元格的情况下) - 元素的基本
类型
(例如,静态文本、警报等) - 元素的
标签
(例如,视图的可访问性标签) - 它代表的底层
XCUIElement
。
如果您正在创建自己的 Element
类型,TABTestKit 会为您大部分情况自动解决,因此您至少只需提供 ID 和类型。
struct MyElement: Element {
let id: String? = "MyElementID"
let type: XCUIElement.ElementType = .other
}
Element
具有一些默认值,意味着可以自动识别 underlyingXCUIElement
。您可以根据需要覆盖这些默认值。
通常不需要创建自己的元素,但如果您确实需要并且认为这对人们有益,请考虑贡献力量并创建一个 PR!
元素状态
Element
协议提供了一些断言和确定状态的函数,任何符合 Element
的都可以访问。
您可以等待元素达到(或未达到)特定状态。
// If using Swift 5.4 or below
button.await(.visible, .enabled, timeout: 10) // You can provide more than one state to wait for :)
button.await(not: .enabled, timeout: 10)
// If using Swift 5.5
button.waitFor(.visible, .enabled, timeout: 10) // You can provide more than one state to wait for :)
button.waitFor(not: .enabled, timeout: 10)
如果元素在超时时间内未能达到预期的状态,测试将失败。
如果您没有使用 TABTestKit 的 上下文,强烈建议使用 Element
提供的函数,而不是立即断言值。
不等待值和状态为真,是导致自动化测试不可靠和令人沮丧的主要原因之一。
可触摸
任何符合 Tappable
(如 Button
)的东西都声明它可以被触摸,包括单次、双击和双指触摸,以及长按。
Tappable
与 InteractionContext
一起使用非常好。
此外,任何符合 Tappable
的 Element
都不需要做额外的工作,默认实现将自动提供。
可编辑
任何符合 Editable
(如 TextField
)的东西都声明它可以输入/移除文本。
Editable
与 InteractionContext
一起使用非常好。
此外,任何符合 Editable
的 Element
都不需要做额外的工作,默认实现将自动提供。
可滚动
任何符合 Scrollable
的内容(例如 ScrollView
),都声明了它可以进行滚动。
Scrollable
与 InteractionContext
结合使用非常出色,您还可以使您的屏幕符合 Scrollable
以实现直接滚动。
extension MyScreen: Scrollable {
func scroll(_ direction: ElementAttributes.Direction) {
scrollView.scroll(direction)
}
}
此外,任何符合 Scrollable
的 Element
都无需进行任何额外操作,将自动提供默认实现。
ValueRepresentable
任何符合 ValueRepresentable
的内容(例如 Label
),都声明了它具有某种类型的值。
符合 ValueRepresentable
的类型决定了在符合时值的数据类型。对于 Header
来说,它是 String
类型,而 Slider
的值是 CGFloat
。
无论值是什么,它都可以与 [InteractionContext
] 结合使用来断言 ValueRepresentable
物品的值。
Adjustable
任何符合 Adjustable
的内容(例如 Slider
),都声明了它可以调整或更改其值。
在符合 Adjustable
时,符合的类型决定了其值的数据类型。在 Slider
的情况下,它是 CGFLoat
类型,而 Switch
的值是 Switch.State
(可以是 .on
或 .off
)。
无论值是什么,都可以与 [InteractionContext
] 结合使用来调整/更改值。
CellContaining
任何符合 CellContaining
的内容(例如 Table
),都声明了它可以提供其包含的单元格数量,并且还可以提供 Cell
。
注意:由于表格和集合视图工作原理以及重复使用视图,XCUI 与表格或集合中的总行数或项目数相比,并不一定有相同数量的单元格。因此,通过索引找到“最后一个”单元格并不可靠,您可以使用单元格的标识符来识别它为最后一个单元格,例如使用 indexPath。
此外,任何符合 CellContaining
的 Element
都无需进行任何额外操作,将自动提供默认实现。
应用
TABTestKit 包含一些预定义的应用,所有这些应用都继承自 BaseApp
,如果您需要创建更多应用,则建议您创建它的子类。
BaseApp
这是所有预定义应用继承的基础应用类。
从 BaseApp
继承意味着您在深入下一步之前,将获得应用处于正确状态的加强等待,这还意味着您将获得一个额外的函数来使应用进入后台,这是 Apple 没有在 XCUIApplication
中提供的。
此外,BaseApp
(以及所有继承自它的对象)符合Element
协议,这意味着任何应用(包括 SystemPreferences
)都可以设置为元素的父元素。
App
App
继承自 BaseApp
,您应在测试中使用它来引用您的应用,要这样做,您必须使用共享实例。
App.shared
您可以将 App
作为任何 Element
的父元素使用,以确保您匹配到您应用中的元素。
SystemPreferences
SystemPreferences
继承自 BaseApp
,并表示系统偏好设置应用。
您可以将 SystemPreferences
作为任何 Element
的父元素使用,这使得您可以在系统偏好设置中交互(是的,真的!)。
Safari
Safari
继承自 BaseApp
,并表示 Safari 应用。
您可以使用 Safari
作为任何 元素
的父级,以便您在 Safari 中与元素交互。
Springboard
Springboard
继承自 BaseApp
并是一个非常特殊的程序,因为它本质上代表了操作系统。
您可以将 Springboard
用作任何 元素
(包括 Alert
)的父级,这意味着您可以与操作系统在您应用中显示的任何警报进行交互,例如 Face ID 权限提示。
需求
TABTestKit 没有任何依赖关系,支持 iOS 10 及以上版本。
安装
Swift 包管理器
您可以在 Xcode 11 或更高版本中将 TABTestKit 作为远程 Swift 包依赖项添加。
由于 SPM 要求混合语言包以特定方式构建,如果您想使用 Biometrics
类,则需要 导入 Biometrics
,直到我们公开 Swift(问题 #118)。
如果您只是使用 BiometricsContext
中的辅助函数,则不需要 导入 Biometrics
。
CocoaPods
最新版本
要使用最新版本的 TABTestKit,只需将以下内容添加到您的 Podfile
文件中,然后在终端中运行 pod update
或 pod install
。
pod 'TABTestKit'
开发版本
要使用开发中的版本,可以特别针对 develop
分支。
pod 'TABTestKit', :git => 'https://github.com/theappbusiness/TABTestKit.git', :branch => 'develop'
Subspecs
有几个 Cocoapod subspec 可用。这意味着如果您只想使用框架的指纹自动化功能,例如,您可以获取 **TABTestKit**
功能的一个子集。
生物识别子规格
使用此子规格仅安装 TABTestKit 的生物识别自动化功能。
pod 'TABTestKit/Biometrics'
BDD 子规格
使用此子规格仅安装 TABTestKit 的 BDD 步骤和场景功能。
pod 'TABTestKit/BDD'
XCUIExtensions 子规格
使用此子规格仅安装 TABTestKit 使用的 XCTest/XCUI 扩展。
pod 'TABTestKit/XCUIExtensions'
Carthage
最新版本
要使用Cartfile.private
中,并在终端中运行carthage bootstrap
或carthage update
。
github "TABTestKit"
如果您是第一次在项目中使用Carthage,您需要按照Carthage说明进行一些额外操作。
注意:目前,Carthage没有提供仅构建特定存储库子模块的方式。
TABTestKit
、BDD
和Biometrics
将整合到一个单独的框架中。
开发
要使用开发中的版本,可以特别针对 develop
分支。
github "TABTestKit" "develop"
贡献
贡献指南可以在这里找到:CONTRIBUTING.md。
贡献者
Neil Horton,[email protected],https://github.com/neil3079
Zachary Borrelli,[email protected],https://github.com/zacoid55
Kane Cheshire,[email protected],https://github.com/kanecheshire
Suyash Srijan,[email protected],https://github.com/theblixguy
许可证
TABTestKit在MIT许可证下可用。有关更多信息,请参见LICENSE文件。