TABTestKit 1.8.0

TABTestKit 1.8.0

测试测试过的
Lang语言 SwiftSwift
许可 MIT
发布最新发布2021年9月
SPM支持 SPM

Kane CheshireZachary BorrelliZachary BorrelliJohn Sanderson 维护。



  • zacoid55, KaneCheshire, theblixguy, neil3079, annpiktas, roger-tan 和 jsanderson44

TABTestKit - Kin + Carta Create

TABTestKit

TABTestKit CI Version Carthage compatible Swift Package Manager compatible License Platform

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元素,如ButtonSliderScrollView

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 在每个测试的开始和结束时自动启动和终止您的应用。您可以延迟启动或终止以提供额外的设置或拆除(例如重置模拟服务器),通过覆盖 preLaunchSetuppreTerminationTearDown 实现。

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
  }

}

注意:如果您覆盖 preLaunchSetuppreTerminationTearDown 并需要在启动或终止应用之前执行一些异步工作,您将需要通过使用 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,您描述屏幕由哪些元素组成。

要定义屏幕,只需创建一个符合Screenstruct

struct ProfileScreen: Screen {

  let trait = Header(id: "My Profile")

}

Screen有一个必须实现的属性,即其trait。一个trait可以是任何可以一致且唯一地标识屏幕的Element,并在使用context进行测试时用于等待它出现在屏幕上。

元素

Element(元素)在 TABTestKit 中代表屏幕上所有不同的元素,如 ViewHeaderSliderAlert 等等。

与 XCTest 不同,其中的所有 XCUIElement 对象都可以进行点击、滑动和输入,TABTestKit 中的 Element 会声明它是否可以滑动(即 ScrollViewTextViewTableView 等),是否具有值(即 HeaderLabelSwitch 等)等等。

您可以使用元素来描述您的屏幕

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 同时符合 ElementTappable

如果您需要任何元素的额外行为,可以创建一个扩展并添加更多功能,例如通过符合另一个协议或创建新的属性和函数。

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作为标识符。

为了使和XCTest能够找到您的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作为标识符。

为了使和XCTest能够找到您的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()

为了使和XCTest能够找到您的,您的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))

此外,您还可以使用KeyboardKeyboardContext

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 中与 incrementButtondecrementButton 交互来进行操作。

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)

由于 PickerWheel 符合 ValueRepresentable,您可以获取字符串值

XCTAssertEqual(wheel.value, "The value")

由于 PickerWheel 也符合 Adjustable,您可以将值调整为另一个 String

wheel.adjust(to: "New value")

日期选择器

DatePicker 代表界面中的日期选择器

let datePicker = DatePicker(id: "MyPicker")

在 iOS 13 和更低版本。您不是直接与选择器交互,而是交互选择器内的轮盘。要交互轮盘,首先请求选择器获取轮盘

let wheel = datePicker.wheel(0)

由于 DatePickerWheel 符合 ValueRepresentable,您可以获取字符串值

XCTAssertEqual(wheel.value, "The value")

由于 DatePickerWheel 也符合 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,您将自动访问到与步骤场景以及元素和其他协议完美配合的大量辅助函数

通常,您甚至不需要自己编写任何额外的函数,这将使您快速地为项目进行自动化变得容易。

导航上下文

NavigationContextTABTestCase已经符合的预定义上下文之一,这意味着您的测试用例可以预先使用其中的函数。

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,您可以完成并删除所有符合CompletableDismissable的任何内容

complete(myScreen)
dismiss(myScreen)
dismiss(myScreen.alert)

Given(I: complete(myScreen))
Given(I: dismiss(myScreen))
Given(I: dismiss(myScreen.alert))

您也可以将任意数量的CompletableDismissable传递给函数,这让您能够轻松地为应用建立流程

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 在幕后由协议驱动。除非您正在创建自己的元素,否则您可能只需要自己实现一些,例如 ScreenCompletableDismissable

屏幕

您使用 Screen 来描述应用中的特定屏幕。

每个 Screen 必须有一个特质来标识它,此特质可以是符合 Element 的任何事物。

struct MyScreen: Screen {

  let trait = View(id: "MyViewControllerView")

}

通常,您的屏幕的特质会是 ViewHeader,但它可以是任何可以持续标识您屏幕的事物。

traitTABTestKitNavigationContext 中用于断言屏幕当前是否在屏幕上。

可滚动屏幕

可滚动屏幕类似于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)的东西都声明它可以被触摸,包括单次、双击和双指触摸,以及长按。

TappableInteractionContext 一起使用非常好。

此外,任何符合 TappableElement 都不需要做额外的工作,默认实现将自动提供。

可编辑

任何符合 Editable(如 TextField)的东西都声明它可以输入/移除文本。

EditableInteractionContext 一起使用非常好。

此外,任何符合 EditableElement 都不需要做额外的工作,默认实现将自动提供。

可滚动

任何符合 Scrollable 的内容(例如 ScrollView),都声明了它可以进行滚动。

ScrollableInteractionContext 结合使用非常出色,您还可以使您的屏幕符合 Scrollable 以实现直接滚动。

extension MyScreen: Scrollable {

  func scroll(_ direction: ElementAttributes.Direction) {
    scrollView.scroll(direction)
  }

}

此外,任何符合 ScrollableElement 都无需进行任何额外操作,将自动提供默认实现。

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。

此外,任何符合 CellContainingElement 都无需进行任何额外操作,将自动提供默认实现。

应用

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 updatepod 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 bootstrapcarthage update

github "TABTestKit"

如果您是第一次在项目中使用Carthage,您需要按照Carthage说明进行一些额外操作。

注意:目前,Carthage没有提供仅构建特定存储库子模块的方式。TABTestKitBDDBiometrics将整合到一个单独的框架中。

开发

要使用开发中的版本,可以特别针对 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文件。