Swift中Xcode UI测试入门

UI测试是确保您最关键的UI交互在添加新功能或重构应用程序的代码库时仍能正常工作的好方法。 这也是一种在处理UI代码时自动执行重复任务的好方法(例如,当您必须深入浏览应用程序以测试正在处理的内容时)。

编写和运行UI测试与进行单元测试有所不同,因为您实际上是在与应用进行交互 ,而不是针对某个API执行编程测试。 两者都有很大的价值,最好的方法是将两者都用于不同的任务。

Xcode附带了XCTest框架中内置的UI测试,您可能已经将其用于单元测试。 这些UI测试功能已经存在了好几年,但是由于不稳定,易碎且难以使用而被许多开发人员所摒弃。 过去是这样 ,在Xcode UI测试的前几次迭代中,确实确实非常不稳定-但现在情况有所好转-如果您还没有这样做的话,我真的值得给它第二次机会🙂

设置东西

如果您的应用程序还没有UI测试目标,则只需添加Xcode中的File > New > Target..并选择一个“ UI测试包”即可。 然后在Xcode中转到Product > Scheme > Edit Scheme.. ,然后在“测试”下添加UI测试包,以编辑应用的方案以在测试时运行UI测试。

让我们写一个测试

UI测试是一个很好的解决方案的例子是,当您想测试用户流程时,例如,在应用程序的入门流程中进行测试。

假设您的入职流程由4个屏幕组成,用户必须先滑动才能完成此过程。 最后,右上角出现一个“完成”按钮,需要关闭该按钮才能关闭入职流程,如下所示:

因此,让我们编写一个精确地执行此操作的测试:

 class OnboardingUITests: XCTestCase { 
var app: XCUIApplication!

// MARK: - XCTestCase

override func setUp() {
super.setUp()

// Since UI tests are more expensive to run, it's
// usually a good idea to exit if a failure was encountered
continueAfterFailure = false

app = XCUIApplication()

// We send a command line argument to our app,
// to enable it to reset its state
app.launchArguments.append("--uitesting")
}

// MARK: - Tests

func testGoingThroughOnboarding() {
app.launch()

// Make sure we're displaying onboarding
XCTAssertTrue(app.isDisplayingOnboarding)

// Swipe left three times to go through the pages
app.swipeLeft()
app.swipeLeft()
app.swipeLeft()

// Tap the "Done" button
app.buttons["Done"].tap()

// Onboarding should no longer be displayed
XCTAssertFalse(app.isDisplayingOnboarding)
}
}

正如您在上面看到的,我们通过与UI实际交互,执行滑动和点击而不是调用我们自己的API来执行UI测试。 实际上,我们的测试将在完全不同的过程中运行,因此我们对自己的代码的访问为零,这在很大程度上是设计使然。 这“迫使”我们实际测试应用程序的使用情况,而不是“伪造”它。

让我们仔细看一下以上两行,从这一行开始:

 app.launchArguments.append("--uitesting") 

如测试中的注释中所述,我们这样做是为了使我们的应用程序能够知道它即将运行UI测试,以便可以重置其状态。 这通常是一个好习惯,因为否则您将最终只能在某些条件下通过测试(=脆弱)。

重置您的应用状态

那么,我们如何真正重置状态? 最简单的方法是在应用启动时简单地检查--uitesting命令行参数,例如在application(didFinishLaunchingWithOptions:) ,如下所示:

 func application(_ application: UIApplication, 
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]?) -> Bool {
if CommandLine.arguments.contains("--uitesting") {
resetState()
}

// ...Finish setting up your app

return true
}

确切地说,在resetState()做什么将根据您的应用持续存在的状态而有所不同。 在这里,您要清除用户默认设置,数据库和所有其他已保存的内容(例如磁盘上的文件)。 例如,这是重置UserDefaults

 let defaultsName = Bundle.main.bundleIdentifier! 
UserDefaults.standard.removePersistentDomain(forName: defaultsName)

在UI测试中验证状态

回到测试中,我想深入探讨的另一行是:

 XCTAssertTrue(app.isDisplayingOnboarding) 

这就是我们用来验证我们是否处于正确状态以及我们的UI行为是否符合我们预期的方法。 它在XCUIApplication的扩展中作为计算属性实现。 我真的建议您使用这种模式,因为它可以使您的测试用例简短明了,并且可以轻松地在多个测试中重用验证代码。

以下是isDisplayingOnboarding的实现方式:

 extension XCUIApplication { 
var isDisplayingOnboarding: Bool {
return otherElements["onboardingView"].exists
}
}

上面的代码使测试运行程序寻找具有accessibilityIdentifier “ onboardingView”的UIView 。 现在我们需要做的就是将此标识符添加到OnboardingViewControllerview

 final class OnboardingViewController: UIViewController { 
override func viewDidLoad() {
super.viewDidLoad()
view.accessibilityIdentifier = "onboardingView"
}
}

结论

为了使您的UI测试易于维护和快速运行,我真的建议使它们尽可能简单,并将复杂的逻辑验证留给单元测试。 由于UI测试的运行速度较慢(因为它们必须实际运行您的应用程序并等待动画等),因此这种方式为您带来了最佳的“回报”,并且使调试的痛苦减轻了很多。

我对测试的总体评价是最喜欢的,这也适用于UI测试,这是在向用户交付应用程序时给了我更大的信心。 使用本文中的示例,我不必不断地手动验证入职是否正常,我可以在CI服务器上的每个PR上运行此测试,而不必担心。

如您在上面的示例中所见,UI测试还使用UIKit可访问性功能进行导航,这意味着,如果您花一些时间来实施一些UI测试,则可以将其与增加应用程序可访问性相结合,这在两个方面很快一! (有关即将发布的帖子中的可访问性的更多信息)。

我已将以上所有示例放到GitHub上的示例应用程序项目中 ,因此请检查一下是否要在上下文中查看所有详细信息。

如果您有任何疑问,建议或反馈,请随时在Twitter上与我联系。 如果您有任何想让我在即将发布的帖子中涉及的主题,我也很乐意收到您的来信! 👍

谢谢阅读! 🚀