Swift中的UI测试—详细指南

581 阅读9分钟

在这篇关于Swift中的单元测试的文章中,我们介绍了如何开始单元测试,使用为测试单个代码单元而编写的例子。然而,在你作为一个iOS开发者的旅程中,你也会想要测试用户界面。UI测试与你的应用程序互动,类似于你的用户如何做。在这篇文章中,我们将看看你如何测试这些类型的场景。我们将为浏览应用程序和与屏幕上的元素进行交互编写测试。

在这篇关于UI测试的介绍性文章中,我们将涵盖以下内容:

  • 添加一个UITesting目标
  • 了解XCUIElement
  • 编写你的第一个UI测试
  • 编写第二个UI测试
  • 录制一个UI测试
  • 拍摄屏幕截图
  • 使用fastlane和Semaphore实现UI测试的自动化

让我们开始吧!

什么是UI测试?

顾名思义,UI测试就是测试用户界面,并通过测试与屏幕上的元素进行互动。这将帮助你确保和验证应用程序的UI部分按照预期工作,并发现由于UI相关代码的变化而导致的任何回归。

苹果公司为我们提供了一个名为XCTest 的本地框架用于UI测试。它依赖于无障碍技术用来与屏幕互动的数据。使用这些数据的工作类似于编写单元测试。你通过子类化XCTestCase ,并添加你的UI测试方法来创建UI测试类。使用这种方法,你写代码与元素互动,并使用断言来验证预期的结果。

添加一个UITesting目标

每当你创建一个新项目时,你可以选择包括测试。这包括单元测试和UI测试。

如果你已经有一个项目,你也可以添加一个UI测试包到它。转到文件>新建>目标。然后,搜索UI测试包

选择Unit Testing Bundle,然后点击Next

为你的项目创建一个新的UI测试目标包括两个带有默认模板的文件。一个是用于常规的UI测试,另一个是用于启动测试。前者由测试用例的生命周期的两个方法组成。

setUpWithError() - 在测试用例的每个测试方法之前,你可以重置状态和抛出错误。状态可以是方向或颜色方案。例如,为每个测试方法设置方向。

XCUIDevice.shared.orientation = .landscapeRight

由于UI测试需要时间,通常最好在失败发生时立即停止。另外,UI测试中的步骤的流程是有顺序的。如果其中一个失败了,就没有必要再运行其他的。你可以通过在setUpWithError() 方法中把continueAfterFailure 设为false 来做到这一点。

tearDownWithError() - 在一个测试案例中的每个测试方法结束之前,你可以进行清理,并将错误扔到这个实例方法中。

接下来,模板中有testExample() ,它包含一个XCUIApplication 的实例。我们将在下一节中了解它。

了解XCUIElement

UI测试的实现是基于三个主要的类:

  • XCUIElement
  • XCUIApplication
  • XCUIElementQuery

XCUIElement 是基类,是应用程序中的UI元素,为你提供iOS上手势交互和macOS上鼠标和键盘交互的功能。常见的方法有: , ,以及与滑动手势有关的各种方法。要确定一个特定的元素是否存在于应用程序的当前UI层次结构中,请使用 实例属性。tap() click() exists

XCUIApplication 是 的一个子类,帮助你在UI测试中启动、监控和终止你的应用程序。XCUIElement

你可以在你的测试类中创建一个实例,然后在setUpWithError() 方法中对其调用launch()

class UITestingExampleUITests: XCTestCase {
  let app = XCUIApplication()

  override func setUpWithError() throws {
    continueAfterFailure = false
    app.launch()
  }
}

要查询当前屏幕上的UI元素,请使用XCUIElementQuery 类。你可以通过它的可访问性标识符查询元素,或者使用一个谓词。有许多元素你可以查询,如静态文本和按钮。这里有一个现成的查询列表供你参考。

现在你知道了基本原理,是时候写出你的第一个UI测试了!

编写你的第一个UI测试

在这个例子中,我们将测试TallestTowers,一个显示全世界最高的塔和有关信息的应用程序。你可以在这里下载该项目。

在TallestTowers项目中,我们看到一个最高的塔楼的列表。我们想通过导航进入详细视图,然后回到主视图来测试导航。为此,我们点击列表中的一行,显示该塔的详细信息,然后点击返回按钮;然后我们可以对另一个塔的标签重复这一操作。

当写一个测试时,在方法前加上 "test "这个词,这样Xcode就会理解它是一个可测试的函数。让我们创建一个方法testNavigation()

func testNavigation() {
  // 1
  let burjKhalifaPredicate = NSPredicate(format: "label beginswith 'Burj Khalifa'")

  // 2
  app.tables.buttons.element(matching: burjKhalifaPredicate).tap()

  // 3
  app.navigationBars.buttons["Tallest Towers"].tap()

  // 4
  let shanghaiTowerPredicate = NSPredicate(format: "label beginswith 'China'")
  app.tables.buttons.element(matching: shanghaiTowerPredicate).tap()
  app.navigationBars.buttons["Tallest Towers"].tap()
}

下面是代码正在做的事情:

  1. 使用NSPredicate 来查询以Burj Khalifa 开始的标签。
  2. XCUIApplication() 的实例中,查询表格中的元素,按钮,找到与谓词匹配的元素,然后点击该元素。这就把屏幕推到了详细视图。
  3. XCUIApplication() 的实例中,查询导航条,找到与 "最高的塔 "相匹配的按钮,然后点击返回按钮。这样屏幕就会弹回到主列表视图。
  4. 再从主视图开始,用另一个元素重复这个过程。

通过点击测试功能线的播放按钮来运行测试。恭喜你!你已经写出了你的第一个UI测试。你已经写了你的第一个UI测试。

让我们再写一个。

编写另一个UI测试

这个测试断言细节中的元素是否存在并与父元素匹配:

func testTowerDetailView() {
  // 1
  let chinaZunPredicate = NSPredicate(format: "label beginswith 'China Zun'")
  app.tables.buttons.element(matching: chinaZunPredicate).tap()

  // 2
  XCTAssert(app.staticTexts["China Zun"].exists)
  XCTAssert(app.staticTexts["Beijing, China"].exists)
  XCTAssert(app.staticTexts["528m"].exists)
  XCTAssert(app.staticTexts["Constructed in"].exists)
  XCTAssert(app.staticTexts["2018"].exists)
}

弄清楚这个测试:

  1. 使用NSPredicate ,查询以China Zun 开始的标签。然后,我们点击与标签匹配的元素。
  2. 点选该元素后,会导航到中国尊塔的详细视图。我们写一些断言语句来检查这个屏幕上是否存在给定的详细静态文本。

命名的提示

无论何时你在写一个UI测试,都要遵循这些最佳实践:

  • 在方法前加上 "test "这个词,这样Xcode就会明白它是一个可测试的函数。另外,一定要写长的方法名称。
  • 要有针对性。如果在众多测试中只有一个测试失败了,那么这个名字应该足以让你知道什么失败了。例如,如果testTowerDetailView() ,你知道它与从主屏幕到详细屏幕的导航流有关。

录制一个UI测试

Xcode为您提供了记录与屏幕的交互的选项,并自动将它们转换为相关的XCUIElementQueryXCUIElement实例及其方法。然后,你可以为预期的结果添加你的断言。

当你准备好测试时,进入一个测试类,把光标放在测试方法里面,记录交互。在调试栏中,点击Record UI Test按钮。

Xcode将启动该应用程序并运行它。您可以与屏幕上的元素进行交互,并为任何UI测试执行一个交互序列。每当你与一个元素互动时,Xcode就会为它写下相应的代码到你的方法中。要停止记录,再次点击Record UI Test按钮。

最后,为选定的UI测试添加你想要的断言:

func testTowerDetailView() {
  let app = XCUIApplication()
  let tablesQuery = app.tables
  tablesQuery.cells["Burj Khalifa, Dubai, United Arab Emirates, 828m"].children(matching: .other).element(boundBy: 0).children(matching: .other).element.tap()

  XCTAssert(app.staticTexts["Burj Khalifa"].exists)
  XCTAssert(app.staticTexts["Dubai, United Arab Emirates"].exists)
  XCTAssert(app.staticTexts["828m"].exists)
  XCTAssert(app.staticTexts["Constructed in"].exists)
  XCTAssert(app.staticTexts["2010"].exists)
}

使用fastlane拍摄截图

App Store Connect需要各种设备的屏幕截图,如果你手动抓取屏幕截图,会很费时、麻烦,而且容易出错。由于应用程序的频繁发布,这个过程变得重复。当你对不同语言的屏幕截图进行本地化时,这种痛苦会变得更加严重。

好在你可以将这个过程自动化。fastlane为我们提供了一个快照功能,可以让你捕捉到数百个多语言的屏幕截图。

要开始的话,在你的项目文件夹中运行以下命令:

fastlane snapshot init

这将在你的项目文件夹中创建一个SnapshotHelper.swift文件。把这个文件添加到UI测试目标中。然后,为UI测试目标添加一个新的Xcode方案,如果它还不在那里。编辑该方案,点击 "Build "侧边栏,并启用 "Run "栏下的复选框。最后,启用新创建方案的共享框。

要创建测试方法的屏幕截图,请调用snapshot() 方法:

func testTakeScreenshots() {
  let app = XCUIApplication()
  setupSnapshot(app)
  app.launch()

  snapshot("01-ListOfTowers")

  let burjKhalifaPredicate = NSPredicate(format: "label beginswith 'Burj Khalifa'")
  app.tables.buttons.element(matching: burjKhalifaPredicate).tap()

  snapshot("02-TowerDetail")
}

上面的代码启动了应用程序,并对主屏幕进行了截图。然后,它查询以Burj Khalifa 开始的标签,并点选导航到下一个屏幕的元素。最后,我们再次调用snapshot() 方法,对详细视图进行截图。

为了拍摄所需的屏幕截图,在终端运行以下命令:

fastlane snapshot

你可以在这里了解更多关于上传屏幕截图到App Store的信息。

在CI中用fastlane和Semaphore实现UI测试的自动化

为了实现测试过程的自动化,我们将使用fastlane,它可以简化部署。我们已经看到fastlane如何帮助实现截图过程的自动化,它在这里也可以帮助我们。

安装fastlane的方法有很多,我们在这里将使用Homebrew。打开终端,运行以下命令:

brew install fastlane

把目录改成项目,然后运行:

fastlane init

现在,打开位于项目文件夹中的Fastfile ,并在其中添加以下几行:

lane :tests do
  run_tests(scheme: "TallestTowers")
end

最后,为了运行测试,在终端执行以下命令:

fastlane tests

为了用持续集成实现流程自动化,请使用Semaphore。请参考这篇关于设置Semaphore的文章。使用CI/CD构建、测试和部署一个iOS应用程序

结论

当最后期限临近时,很难专注于编写测试,但从长远来看,这是有好处的。编写UI测试可以确保你在应用中保持关键UI流程的完整性。当添加一个新功能或重构代码时,测试可以帮助你提前发现任何回归,并随着时间的推移加快你的开发过程。

此外,留出一些时间为你的应用程序配置CI/CD是一个好主意。这可以让你专注于提供良好的用户体验,而不是在每次发布时手动测试应用程序的用户界面。

现在就去,充满信心地编写UI测试吧