- 原文地址:Running UITests with Facebook login in iOS
- 原文作者:Khoa Pham
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者: LoneyIsError
- 校對者:Alan
圖片來源: 谷歌
今天我正試圖在我的應用程式上執行一些 UITest,它整合了 Facebook 登入功能。以下是我的一些筆記。
挑戰
- 對我們來說,使用 Facebook 的挑戰主要在於, 它使用了
Safari controller
,而我們主要處理web view
。從 iOS 9+ 開始,Facebook 決定使用safari
取代native facebook app
以此來避免應用間的切換。你可以在這裡閱讀詳細資訊 在iOS 9上為人們構建最佳的 Facebook 登入體驗 - 它並沒有我們想要的
accessibilityIdentifier
或者accessibilityLabel
- webview 內容將來可能會發生變化 ?
建立一個 Facebook 測試使用者
幸運的是,您不必建立自己的 Facebook 使用者用於測試。Facebook 支援建立測試使用者,可以管理許可權和好友,非常方便
當我們建立測試使用者時,您還可以選擇不同語言。這將是 Safari Web 檢視中顯示的語言。我現在選擇的是 Norwegian
??
單擊登入按鈕並顯示 Facebook 登入
這裡我們使用預設的 FBSDKLoginButton
var showFacebookLoginFormButton: XCUIElement {
return buttons["Continue with Facebook"]
}
複製程式碼
然後點選它
app.showFacebookLoginFormButton.tap()
複製程式碼
檢查登入狀態
當在 Safari 訪問 Facebook 表單時,使用者也許已經登入過,也許沒有。所以我們需要處理這兩種情況。所以我們需要處理這兩個場景。當使用者已經登入時,Facebook 會返回你已經登入
或 OK
按鈕。
這裡的建議是新增斷點,然後使用 lldb
命令 po app.staticTexts
和 po app.buttons
,檢視當前斷點下的 UI 元素。
您可以檢查靜態文字,或只是點選 OK
按鈕
var isAlreadyLoggedInSafari: Bool {
return buttons["OK"].exists || staticTexts["Du har allerede godkjent Blue Sea."].exists
}
複製程式碼
等待並重新整理
因為 Facebook 表單是一個 webview ,所以它的內容是有點動態的。並且 UITest 似乎會快取內容以便快速查詢,因此在檢查 staticTexts
之前,我們需要 wait
和 refresh the cache
app.clearCachedStaticTexts()
複製程式碼
這裡實現了 wait
功能
extension XCTestCase {
func wait(for duration: TimeInterval) {
let waitExpectation = expectation(description: "Waiting")
let when = DispatchTime.now() + duration
DispatchQueue.main.asyncAfter(deadline: when) {
waitExpectation.fulfill()
}
// We use a buffer here to avoid flakiness with Timer on CI
waitForExpectations(timeout: duration + 0.5)
}
}
複製程式碼
等待元素出現
但更保險的方法是等待元素出現。對於 Facebook 登入表單來說,他們會在載入後顯示 Facebook
的標籤。所以我們應該等待這個元素出現
extension XCTestCase {
/// Wait for element to appear
func wait(for element: XCUIElement, timeout duration: TimeInterval) {
let predicate = NSPredicate(format: "exists == true")
let _ = expectation(for: predicate, evaluatedWith: element, handler: nil)
// Here we don't need to call `waitExpectation.fulfill()`
// We use a buffer here to avoid flakiness with Timer on CI
waitForExpectations(timeout: duration + 0.5)
}
}
複製程式碼
在對 Facebook 登入表單中的元素進行任何進一步檢查之前,請呼叫此方法
wait(for: app.staticTexts["Facebook"], timeout: 5)
複製程式碼
如果使用者已登入
登入後,我的應用程式會在主控制器中顯示一個地圖頁面。因此,我們需要簡單的測試一下,檢查該地圖是否存在
if app.isAlreadyLoggedInSafari {
app.okButton.tap()
handleLocationPermission()
// Check for the map
XCTAssertTrue(app.maps.element(boundBy: 0).exists)
}
複製程式碼
處理中斷
我們知道,當要顯示位置地圖時,Core Location
會傳送請求許可。所以我們也需要處理這種中斷。你需要確保在彈框彈出之前儘早呼叫它
fileprivate func handleLocationPermission() {
addUIInterruptionMonitor(withDescription: "Location permission", handler: { alert in
alert.buttons.element(boundBy: 1).tap()
return true
})
}
複製程式碼
還有一個問題,這個監視器
不會被呼叫。所以解決方法是在彈框彈起時再次呼叫 app.tap()
。 對我來說,我會在我的 ‘地圖’ 顯示1到2秒後呼叫 app.tap()
,這是為了確保在顯示彈框之後再呼叫 app.tap()
更詳細的指南,請閱讀 #48
如果使用者未登入
在這種情況下,我們需要填寫郵箱賬戶和密碼。 您可以檢視下面的完整原始碼
部分。當如果方法不起作用或者 po
命令並沒有列印出你需要的元素時,這可能是因為快取或者你需要等到動態內容渲染完成後在再嘗試。
您需要等待元素出現
點選文字輸入框
如果遇到這種情況 Neither element nor any descendant has keyboard focus
, 這是解決方法
- 如果你在模擬器上測試, 請確保沒有選中
Simulator -> Hardware -> Keyboard -> Connect Hardware Keyboard
- 點選後稍微
稍等
一下
app.emailTextField.tap()
複製程式碼
清除所有文字
此舉是為了將游標移動到文字框末尾,然後依次刪除每一個字元,並鍵入新的文字
extension XCUIElement {
func deleteAllText() {
guard let string = value as? String else {
return
}
let lowerRightCorner = coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.9))
lowerRightCorner.tap()
let deletes = string.characters.map({ _ in XCUIKeyboardKeyDelete }).joined(separator: "")
typeText(deletes)
}
}
複製程式碼
修改語言環境
對我來說,我想用挪威語進行測試,所以我們需要找到 Norwegian
選項並點選它。它被 UI Test
識別為靜態文字
var norwegianText: XCUIElement {
return staticTexts["Norsk (bokmål)"]
}
wait(for: app.norwegianText, timeout: 1)
app.norwegianText.tap()
複製程式碼
郵箱賬戶輸入框
幸運的是,郵箱賬戶輸入框可以被 UI Test
檢測為 text field
元素,因此我們可以查詢它。 這裡使用謂詞
var emailTextField: XCUIElement {
let predicate = NSPredicate(format: "placeholderValue == %@", "E-post eller mobil")
return textFields.element(matching: predicate)
}
複製程式碼
密碼輸入框
UI Test
似乎無法識別出密碼輸入框,因此我們需要通過 coordinate
進行搜尋
var passwordCoordinate: XCUICoordinate {
let vector = CGVector(dx: 1, dy: 1.5)
return emailTextField.coordinate(withNormalizedOffset: vector)
}
複製程式碼
下面是這個方法的文件描述func coordinate(withNormalizedOffset normalizedOffset: CGVector) -> XCUICoordinate
建立並返回帶有標準化偏移量的新座標。 座標的螢幕點是通過將 normalizedOffset 乘以元素
frame
的大小與元素frame
的原點相加來計算的。
然後輸入密碼
app.passwordCoordinate.tap()
app.typeText("My password")
複製程式碼
我們不應該使用 app.passwordCoordinate.referencedElement
因為它會指向郵箱賬戶輸入框 ❗️ ?
再次執行該測試
這裡我們從 Xcode -> Product -> Perform Actions -> Test Again
再次執行上一個測試
以下是完整的原始碼
import XCTest
class LoginTests: XCTestCase {
var app: XCUIApplication!
func testLogin() {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
passLogin()
}
}
extension LoginTests {
func passLogin() {
// Tap login
app.showFacebookLoginFormButton.tap()
wait(for: app.staticTexts["Facebook"], timeout: 5) // This requires a high timeout
// There may be location permission popup when showing map
handleLocationPermission()
if app.isAlreadyLoggedInSafari {
app.okButton.tap()
// Show map
let map = app.maps.element(boundBy: 0)
wait(for: map, timeout: 2)
XCTAssertTrue(map.exists)
// Need to interact with the app for interruption monitor to work
app.tap()
} else {
// Choose norsk
wait(for: app.norwegianText, timeout: 1)
app.norwegianText.tap()
app.emailTextField.tap()
app.emailTextField.deleteAllText()
app.emailTextField.typeText("mujyhwhbby_1496155833@tfbnw.net")
app.passwordCoordinate.tap()
app.typeText("Bob Alageaiecghfb Sharpeman")
// login
app.facebookLoginButton.tap()
// press OK
app.okButton.tap()
// Show map
let map = app.maps.element(boundBy: 0)
wait(for: map, timeout: 2)
XCTAssertTrue(map.exists)
// Need to interact with the app for interruption monitor to work
app.tap()
}
}
fileprivate func handleLocationPermission() {
addUIInterruptionMonitor(withDescription: "Location permission", handler: { alert in
alert.buttons.element(boundBy: 1).tap()
return true
})
}
}
fileprivate extension XCUIApplication {
var showFacebookLoginFormButton: XCUIElement {
return buttons["Continue with Facebook"]
}
var isAlreadyLoggedInSafari: Bool {
return buttons["OK"].exists || staticTexts["Du har allerede godkjent Blue Sea."].exists
}
var okButton: XCUIElement {
return buttons["OK"]
}
var norwegianText: XCUIElement {
return staticTexts["Norsk (bokmål)"]
}
var emailTextField: XCUIElement {
let predicate = NSPredicate(format: "placeholderValue == %@", "E-post eller mobil")
return textFields.element(matching: predicate)
}
var passwordCoordinate: XCUICoordinate {
let vector = CGVector(dx: 1, dy: 1.5)
return emailTextField.coordinate(withNormalizedOffset: vector)
}
var facebookLoginButton: XCUIElement {
return buttons["Logg inn"]
}
}
extension XCTestCase {
func wait(for duration: TimeInterval) {
let waitExpectation = expectation(description: "Waiting")
let when = DispatchTime.now() + duration
DispatchQueue.main.asyncAfter(deadline: when) {
waitExpectation.fulfill()
}
// We use a buffer here to avoid flakiness with Timer on CI
waitForExpectations(timeout: duration + 0.5)
}
/// Wait for element to appear
func wait(for element: XCUIElement, timeout duration: TimeInterval) {
let predicate = NSPredicate(format: "exists == true")
let _ = expectation(for: predicate, evaluatedWith: element, handler: nil)
// We use a buffer here to avoid flakiness with Timer on CI
waitForExpectations(timeout: duration + 0.5)
}
}
extension XCUIApplication {
// Because of "Use cached accessibility hierarchy"
func clearCachedStaticTexts() {
let _ = staticTexts.count
}
func clearCachedTextFields() {
let _ = textFields.count
}
func clearCachedTextViews() {
let _ = textViews.count
}
}
extension XCUIElement {
func deleteAllText() {
guard let string = value as? String else {
return
}
let lowerRightCorner = coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.9))
lowerRightCorner.tap()
let deletes = string.characters.map({ _ in XCUIKeyboardKeyDelete }).joined(separator: "")
typeText(deletes)
}
}
複製程式碼
另外一點
感謝這些我原創文章的有用反饋 github.com/onmyway133/…, 這裡有一些更多的點子
- 要查詢密碼輸入框,實際上我們可以使用
secureTextFields
來代替使用coordinate
wait
函式應該作為XCUIElement
的擴充套件,以便於其他元素可以使用它。或者你可以使用舊的expectation
樣式,這不涉及硬編碼的間隔值。
進一步擴充
這些指南涵蓋了 UITests 許多方面的內容,值得一看
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。