ios ui自动化测试
Did you know it’s possible to terminate your app in the middle of an XCUITest and launch it again from somewhere else? Let’s see how to use this trick to test that deep links and universal links are properly launching your app when executed from Safari or iMessage.
您是否知道可以在XCUITest中终止应用程序并从其他地方再次启动它? 让我们看看如何使用此技巧来测试从Safari或iMessage执行深链接和通用链接是否正确启动了您的应用程序。
It’s tricky to test iOS features because while you can write unit tests to guarantee that your abstraction of it works, you can’t really unit-test for whether iOS will correctly call what you think will be called. In the case of deep links, what iOS does differs depending on the current state of your app (closed or in the background) and which delegates you support (AppDelegates
versus SceneDelegates
). This commonly leads to very confusing bug reports from the point of view of the developer who isn’t aware of this fact.
测试iOS功能非常棘手,因为尽管您可以编写单元测试以确保对它的抽象进行工作,但是您无法真正对iOS是否正确调用您认为要调用的内容进行单元测试。 在深层链接的情况下,iOS的操作会因应用程序的当前状态(关闭或处于后台状态)以及您支持的代表( AppDelegates
与SceneDelegates
)而SceneDelegates
。 从不了解这一事实的开发人员的角度来看,这通常会导致非常令人困惑的错误报告。
But unless you’re not supporting iOS 11 in 2020 for some reason, you can perfectly test “app launch”-related features and any other AppDelegate
/SceneDelegate
-related feature through UI Tests. This is because it was in iOS 11 that XCUI started supporting the ability to launch and control system apps.
但是,除非您出于某种原因在2020年不支持iOS 11,否则您可以通过UI测试完美地测试与“应用程序启动”相关的功能以及任何其他与AppDelegate
/ SceneDelegate
相关的功能。 这是因为XCUI在iOS 11中开始支持启动和控制系统应用程序的功能。
Today, we can make a test that boots Safari, types a URL, and deep-links back to our app. We can even terminate our app (which doesn’t stop the test!) to check that our app behaves correctly if it’s launched from said deep link!
今天,我们可以进行测试,以启动Safari,键入URL并深度链接回我们的应用程序。 我们甚至可以终止我们的应用程序(这不会停止测试!),以检查我们的应用程序是否从所述深层链接启动而正常运行!
UI测试深层链接(从后台应用程序) (UI Testing Deep Links (From a Backgrounded App))
To begin, let’s UI test a Safari deep link when our app is already running in the background.
首先,让我们的UI在我们的应用程序已经在后台运行时测试Safari深度链接。
Launching other apps in the middle of a UI test is similar to launching our own, with the simple difference that you pass a different bundle identifier instead. In the case of Safari, the bundle identifier is "com.apple.mobilesafari"
:
在UI测试过程中启动其他应用程序类似于启动我们自己的应用程序,只是区别在于您传递了不同的包标识符。 对于Safari,捆绑包标识符为"com.apple.mobilesafari"
:
func testDeeplinkFromSafari() {
let app = XCUIApplication()
app.launch()
let safari = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")
safari.launch()
}
If you run this, you’ll get a simple test that launches your app and switches to Safari right after.
如果运行此程序,您将获得一个简单的测试,该测试将启动您的应用程序,然后立即切换到Safari。
Now, to deep-link back to our app, we can control Safari just like we would in a regular UI test. In this case, we can grab a hold of the address bar, type our link, and press the “go” button. If everything works correctly, Safari will deep-link back to our app, allowing us to assert that the deep link logic in our app is working as expected.
现在,要深层链接回我们的应用程序,我们可以像在常规UI测试中一样控制Safari。 在这种情况下,我们可以抓住地址栏,输入链接,然后按“执行”按钮。 如果一切正常,Safari就会将其深链接回我们的应用程序,从而使我们能够断言应用程序中的深层链接逻辑正在按预期工作。
func testDeeplinkFromSafari() {
// Launch our app
let app = XCUIApplication()
app.launch()
// Launch Safari and deeplink back to our app
openFromSafari("swiftrocks://profile")
// Make sure Safari properly switched back to our app before asserting
XCTAssert(app.wait(for: .runningForeground, timeout: 5))
// Assert that the deeplink worked by checking if we're in the "Profile" screen
XCTAssertTrue(app.navigationBars["Profile"].exists)
}
private func openFromSafari(_ urlString: String) {
let safari = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")
safari.launch()
// Make sure Safari is really running before asserting
XCTAssert(safari.wait(for: .runningForeground, timeout: 5))
// Type the deeplink and execute it
let firstLaunchContinueButton = safari.buttons["Continue"]
if firstLaunchContinueButton.exists {
firstLaunchContinueButton.tap()
}
safari.buttons["URL"].tap()
let keyboardTutorialButton = safari.buttons["Continue"]
if keyboardTutorialButton.exists {
keyboardTutorialButton.tap()
}
safari.typeText(urlString)
safari.buttons["Go"].tap()
_ = confirmationButton.waitForExistence(timeout: 2)
if confirmationButton.exists {
confirmationButton.tap()
}
}
It’s good to add the additional wait(for: .runningForeground)
assertion for safety, as that makes the test check that the app switching actually worked before attempting to assert anything else. If it fails for some reason, you'll know it was because the app failed to switch instead of something not being present in the UI of your app.
为了安全起见,最好添加额外的wait(for: .runningForeground)
断言,因为这样可以在尝试断言其他任何事情之前测试应用切换是否确实有效。 如果由于某种原因它失败了,您会知道这是因为该应用程序无法切换,而不是您的应用程序UI中不存在某些内容。
You may also notice that there’s some additional logic in our Safari handling. Safari sometimes shows a “What’s new” screen, which we treat by first finding and tapping the "Continue"
button if it exists, which can also happen when opening the keyboard at the same time. Additionally, when executing deep links you might sometimes get an "Open in X?" confirmation, which is treated by finding and tapping the "Open"
button.
您可能还会注意到,我们的Safari处理中还有一些其他逻辑。 Safari有时会显示“新功能”屏幕,我们将首先查找并点按"Continue"
按钮(如果存在)来进行处理,这在同时打开键盘时也会发生。 此外,执行深层链接时,有时可能会显示“在X中打开?”。 确认,方法是找到并点击"Open"
按钮。
UI测试深层链接(启动应用程序/从被终止的应用程序启动) (UI Testing Deep Links (That Launch the App/From a Killed App))
The issue I faced that prompted me to write this article is that iOS processes deep links differently according to the current state of the app. For example, in SceneDelegates
, deep links will trigger your scene(_:openURLContexts:)
method. But if the app is launched as a result of the deep link, no method is called. Instead, you need to access it from the urlContexts
property of your scene. Thus, when UI testing, you also need to have a test that operates on an app that is not running.
我遇到的促使我写这篇文章的问题是,iOS根据应用程序的当前状态处理深层链接的方式有所不同。 例如,在SceneDelegates
,深层链接将触发您的scene(_:openURLContexts:)
方法。 但是,如果应用是由于深层链接而启动的,则不会调用任何方法。 相反,您需要从场景的urlContexts
属性访问它。 因此,在进行UI测试时,您还需要进行在未运行的应用程序上运行的测试。
One may think that a UI test would fail if your app terminates, but that’s actually not the case! You can make a test that terminates and reboots an app as much as you like by using these special methods from XCUIApplication
:
有人可能会认为,如果您的应用终止,则UI测试将失败,但实际上并非如此! 通过使用XCUIApplication
以下特殊方法,您可以根据需要进行终止和重新启动应用程序的测试:
app.launch() // Launches the app (or reboots it/launches it again)
app.terminate() // Terminates the app (which does not stops the test!)
app.actviate() // Puts the app in the foreground, if it was backgrounded
As mentioned, if you terminate the app, you’re free to launch it again in the same test. It’s not necessary to reassign your XCUIApplication
instance. All assertions will work normally as if the app was never terminated in the first place.
如前所述,如果您终止应用程序,则可以在同一测试中再次启动它。 不必重新分配您的XCUIApplication
实例。 所有断言都将正常运行,就好像该应用程序从未被终止一样。
Thus, to test that our deep links work correctly when the app isn’t launched, we can simply close the app before opening Safari. It’s not necessary to launch it again, as that will happen naturally as iOS attempts to open our deep link.
因此,要测试未启动应用程序时我们的深层链接能否正常工作,我们可以在打开Safari之前先关闭应用程序。 无需再次启动它,因为随着iOS尝试打开我们的深层链接,这自然会发生。
func testDeeplinkFromSafari_fromBackgroundedApp() {
openSafariDeeplink(terminateFirst: false)
}
func testDeeplinkFromSafari_thatLaunchesTheApp() {
openSafariDeeplink(terminateFirst: true)
}
func openSafariDeeplink(terminateFirst: Bool) {
let app = XCUIApplication()
app.launch()
if terminateFirst {
app.terminate()
}
// Launch Safari and deeplink back to our app
openFromSafari("swiftrocks://profile")
// Make sure Safari properly switched back to our app before asserting
XCTAssert(app.wait(for: .runningForeground, timeout: 5))
// Assert that the deeplink worked by checking if we're in the "Profile" screen
XCTAssertTrue(app.navigationBars["Profile"].exists)
}
An alternative for this is to simply never call app.launch()
, but I had mixed results with it. Launching the app also installs it, so never launching it resulted in flaky tests. Launching and terminating it, however, works 100% of the time.
一种替代方法是根本不调用app.launch()
,但是我将结果混合在一起。 启动该应用程序还会安装该应用程序,因此切勿启动它导致不稳定的测试。 但是,启动和终止它的时间是100%。
UI测试通用链接(从后台应用程序) (UI Testing Universal Links (From a Backgrounded App))
The testing process of universal links is very similar to that of deep links, with an important difference: For some reason, universal links don’t work in the simulator’s Safari. It’s unclear if that’s on purpose or if it’s really a bug, but while universal links work fine on your device’s Safari, they will not work on the simulator’s one.
通用链接的测试过程与深层链接的测试过程非常相似,但有一个重要区别:出于某种原因,通用链接在模拟器的Safari中不起作用。 尚不清楚这是故意的还是真正的错误,但是虽然通用链接在设备的Safari上可以正常使用,但在模拟器的Safari上却无法使用。
This means we unfortunately can’t use our Safari wrapper for them, but luckily, you can still test universal links by using the Messages app. We can then test our universal links by opening the Messages app, clicking on a contact, sending them a universal link, and tapping the newly sent message’s link bubble to trigger it.
不幸的是,这意味着我们不能为它们使用Safari包装器,但是幸运的是,您仍然可以使用Messages应用程序测试通用链接。 然后,我们可以通过以下方法测试通用链接:打开“消息”应用程序,单击联系人,向他们发送通用链接,然后点击新发送的消息的链接气泡以触发它。
To launch Messages, we use the bundle identifier "com.apple.MobileSMS"
.
要启动消息,我们使用包标识符"com.apple.MobileSMS"
。
private func openFromMessages(_ urlString: String) {
let messages = XCUIApplication(bundleIdentifier: "com.apple.MobileSMS")
messages.launch()
XCTAssert(messages.wait(for: .runningForeground, timeout: 5))
// Dismiss "What's New" if needed
let continueButton = messages.buttons["Continue"]
if continueButton.exists {
continueButton.tap()
}
// Dismiss iOS 13's "New Messages" if needed
let cancelButton = messages.navigationBars.buttons["Cancel"]
if cancelButton.exists {
cancelButton.tap()
}
// Open the first available chat
let chat = messages.cells.firstMatch
XCTAssertTrue(chat.waitForExistence(timeout: 5))
chat.tap()
// Tap the text field
messages.textFields["iMessage"].tap()
// Dismiss Keyboard tutorial if needed
let keyboardTutorialButton = messages.buttons["Continue"]
if keyboardTutorialButton.exists {
keyboardTutorialButton.tap()
}
messages.typeText("Link: \(urlString)")
messages.buttons["sendButton"].tap()
let bubble = messages.links.firstMatch
XCTAssertTrue(bubble.waitForExistence(timeout: 5))
sleep(3)
bubble.tap()
}
The logic to open a link from Messages is a little longer because it sometimes takes a couple more taps before being able to click our link. In this case, we may need to dismiss up to three onboarding screens before being able to send a message. Additionally, before tapping the link, we sleep(3)
to give iOS enough time to load our app's metadata. If you don't wait, sometimes iOS will fail to properly open your app.
从“消息”中打开链接的逻辑要更长一些,因为有时需要多点击几次才能单击我们的链接。 在这种情况下,我们可能需要先关闭多达三个入门屏幕,然后才能发送消息。 此外,在点击链接之前,我们进行sleep(3)
以便为iOS提供足够的时间来加载应用程序的元数据。 如果您不等待,有时iOS将无法正确打开您的应用程序。
The result, however, is the same from when we tested deep links in Safari. When you call this method, iOS will switch to Messages and attempt to switch back to your app via your universal link.
但是,结果与我们在Safari中测试深层链接时的结果相同。 当您调用此方法时,iOS将切换到消息并尝试通过通用链接切换回您的应用。
func testUniversalLinkFromMessages() {
// Launch our app
let app = XCUIApplication()
app.launch()
// Launch Messages and univesal link back to our app
openFromMessages("https://swiftrocks.com/profile")
// Make sure Messages properly switched back to our app before asserting
XCTAssert(app.wait(for: .runningForeground, timeout: 5))
// Assert that the universal link worked by checking if we're in the "Profile" screen
XCTAssertTrue(app.navigationBars["Profile"].exists)
}
UI测试通用链接(启动应用程序/从被终止的应用程序启动)(UI Testing Universal Links (That Launch the App/From a Killed App))
Like with deep links, iOS’s behavior differs slightly when launching your app as a result of tapping a universal link. When using SceneDelegates
, for example, you need to instead fetch them from a scene’s userActivities
property.
与深层链接一样,由于点击通用链接,因此在启动应用程序时,iOS的行为略有不同。 例如,在使用SceneDelegates
,您需要改为从场景的userActivities
属性中获取它们。
To confirm that our app can properly handle this, we can use the same trick we used for the deep links and terminate our app before executing the test.
为了确认我们的应用程序可以正确处理此问题,我们可以使用与深层链接相同的技巧,并在执行测试之前终止我们的应用程序。
func testUniversalLinkFromMessages_fromBackgroundedApp() {
openMessagesUniversalLink(terminateFirst: false)
}
func testUniversalLinkFromMessages_thatLaunchesTheApp() {
openMessagesUniversalLink(terminateFirst: true)
}
func openMessagesUniversalLink(terminateFirst: Bool) {
let app = XCUIApplication()
app.launch()
if terminateFirst {
app.terminate()
}
// Launch Messages and univesal link back to our app
openFromMessages("https://swiftrocks.com/profile")
// Make sure Messages properly switched back to our app before asserting
XCTAssert(app.wait(for: .runningForeground, timeout: 5))
// Assert that the universal link worked by checking if we're in the "Profile" screen
XCTAssertTrue(app.navigationBars["Profile"].exists)
}
翻译自: https://medium.com/better-programming/ui-testing-deep-links-and-universal-links-in-ios-a6052ac65039
ios ui自动化测试