XCUITestのコツ
fastlaneのsnapshotでスクリーンショット取得を自動化するのに必要になったので、使ってみました。
はじめに
UIテストでは、Viewの階層は、XCUIElementというUI要素のツリーとして見えます。このツリーにクエリーをかけてUI要素を特定し、そのUI要素に、exists
で存在をXCTAssert()
で確認したり、tap()
やtypeText()
などで操作することで、テストケースが進んでいきます。
最初にやる手順は、
- File - New - Target - iOS UI Testing Bundleで、UITestを作成する (プロジェクト作成時に作ってあれば、それでOK)
- test...()メソッドの中で、Xcodeのレコーディングボタン(左下の赤い丸)を押し、シミュレータのアプリを操作して、テストコードを生成する
- 運が良ければそのまま実行できます
- エラーになる場合、ブレークポイントで止めて、デバッガでXCUIElementのツリーや要素の状態を調べて、問題を解決していきます
po app
po app.searchFields
po app.searchFields.element
UI要素(XCUIElement)を特定する
XCUIElementのクエリーは、直接の子供だけでなく、配下の子孫まで全部クエリーして、複数の要素を返してくれます。操作したいUI要素を特定するには、いくつか方法があります。
- あるタイプのUI要素が画面に一つしかないなら、
app.buttons.element
の様に、決め打ちでOK。 - テキストでクエリーして取る。
app.buttons["Back"]
- accessibilityIdというプロパティを設定して、それを指定して取る。
- 位置による指定: 何番目のUI要素かで取る。
.elementBoundByIndex(3)
- この場合、直接の子要素の中の順番で指定する方が、変更に強いかもしれません。
.childrenMatchingType(.Other).elementBoundByIndex(3)
- この場合、直接の子要素の中の順番で指定する方が、変更に強いかもしれません。
なぜかUI要素がないエラーになるのを回避する
画面サイズ、言語設定、タイミングなどで、UIテストが失敗することがあります。ある程度柔軟に書いておきましょう。
- 時間のかかるところではsleep(1)等を入れてみる。
- 一応アプリがidleになるのを待ってから、次の処理を実行してくれるのですが、通信に時間がかかったりすると、idleになったと判断して次に進んでしまうことがあるようです。
- 言語が変わるとボタンなどの名前が変わることがあるので、デバッガなどで名前を見てみる。
- tap()する前にexistsをチェックし、可能なら代替手段を使う。
特定の言語でテストを実行する
override func setUp() {
super.setUp()
let app = XCUIApplication()
app.launchArguments += ["-AppleLanguages", "(ja)"]
app.launchArguments += ["-AppleLocale", "ja_JP"]
app.launch()
}
実行する度に結果が変わってしまうのを回避する
アプリに、前回の状態を保存しておいて再開するような機能がある場合、テストの度にスナップショットの結果が変わってくることがあります。この場合、必要であればアプリ側に、特定の状態にリセットする機能を仕込んでおくと良いです。
- TextFieldやTextViewの文字をクリアする (やや面倒なので後述)
- キャッシュを削除する
- 地図のカメラを決まったポジションにする
テキストをクリアするXCUIElementの拡張
テキストをクリアするXCUIElementの拡張です。
extension XCUIElement {
func clearText() {
guard let text = value as? String else {
XCTAssert(false, "Not a text element.")
return
}
if text.isEmpty {
return
}
if buttons["Clear text"].exists {
buttons["Clear text"].tap()
return
}
tap()
let selectAll = XCUIApplication().menuItems["Select All"]
if selectAll.exists {
selectAll.tap()
typeText(XCUIKeyboardKeyDelete)
return
}
}
}
使用例
let app = XCUIApplication()
let searchField = app.searchFields.element
searchField.tap()
searchField.clearText()
searchField.typeText("Ramen")
app.keyboards.buttons["Search"].tap()