XCUITest周りで時間を浪費しないために
XCUIElementTypeQueryProviderの定義済みクエリを使うことで、テストで利用するUIを特定することができます。
しかし、まとまった記事が少なく扱い方が見つからず時間を浪費したものもいくつかあったので自分で発見したやり方も含めて簡単にまとめてみます。
※下記を省略してます
let app = XCUIApplication()
AccessibilityIdentifierを使えるパターン
UIView
view.accessibilityIdentifier = "sample_view"
let view = app.otherElements["sample_view"]
UIButton
let button = UIButton()
button.accessibilityIdentifier = "sample_button"
// サンプルというタイトルをセット
button.setTitle("サンプル", for: .normal)
let button = app.buttons["sample_button"]
// ボタンのタイトルを確認
XCTAssertEqual(button.label, "サンプル")
UILabel
let label = UILabel()
label.accessibilityIdentifier = "sample_label"
// サンプルというテキストをセット
label.text = "サンプル"
let label = app.staticTexts["sample_label"]
// ラベルのテキストを確認
XCTAssertEqual(label.label, "サンプル")
UITextField (SecureTextEntryではない)
テキストの中身は.value as! String
でキャストして使います。
let textField = UITextField()
textField.accessibilityIdentifier = "sample_textField"
// サンプルというテキストをセット
textField.text = "サンプル"
let textField = app.textFields["sample_textField"]
// ラベルのテキストを確認
XCTAssertEqual(textField.value as! String, "サンプル")
UITextField (SecureTextEntry)
テキストの中身は.value as! String
でキャストして使うと
中身が"・・・・"
になるのでバリデーションチェックなどには使えません。
※いい方法あれば教えて欲しいです。
let textField = UITextField()
textField.isSecureTextEntry = true
textField.accessibilityIdentifier = "sample_textField"
// サンプルというテキストをセット
textField.text = "サンプル"
let textField = app.secureTextFields["sample_textField"]
// ラベルのテキストを確認
XCTAssertEqual(textField.value as! String, "サンプル") // failure
UITabBarとUITabBarItem
tabBarController?.tabBar.accessibilityIdentifier = "sample_tabBar"
tabBarItem.accessibilityIdentifier = "sample_barItem"
let tabBar = app.tabBars["sample_tabBar"]
let barItem = tabBar.buttons["sample_barItem"]
UIImageView
.isAccessibilityElement = true
これが無いと使えません。
let imageView = UIImageView()
imageView.isAccessibilityElement = true
imageView.accessibilityIdentifier = "sample_image"
let tabBar = app.tabBars["sample_tabBar"]
let barItem = tabBar.buttons["sample_barItem"]
AccessibilityIdentifierを使えないパターン
SwipeMenu
.accessibilityIdentifier
が設定できないので
.accessibilityLabel
を設定しておくとうまく拾える。
let action = UIContextualAction()
action.accessibilityLabel = "leadingSwipe_action"
let action = app.tables.cells.firstMatch.buttons["leadingSwipe_action"]
AlertとAlertAction
ラベル名でしか拾えない。※いい方法あれば誰か教えて欲しいです。
let alert = UIAlertController()
alert.view.accessibilityIdentifier = "alert_view"
let cancelAction = UIAlertAction(
title: "キャンセル",
style: UIAlertAction.Style.cancel,
handler: nil))
// accessibilityIdentifierを指定しても使えない。
cancelAction.accessibilityIdentifier = "alert_cancel_button"
...
let alert = app.alerts["alert_view"]
// accessibilityIdentifierを指定しても使えない。
// let cancelAction = alert.buttons["alert_cancel_button"]
// ラベル名で拾える
let cancelAction = alert.buttons["キャンセル"]
それでも拾い方がわからない時
例えばUISegmentedControl
内のSFシンボルを使ったボタン
順番で拾うパターン
let segmentedControl = app.segmentedControls["sample_segmentedControl"]
let button01 = segmentedControl.buttons.element(boundBy: 0) // 拾いたいボタンのindex
順番が使えないパターン(動的に変化するなど)
SFシンボルの特有のラベル名を事前に調べておきます。
※SFシンボル名とは若干異なるようです。
let segmentedControl = app.segmentedControls["sample_segmentedControl"]
for button in segmentedControl.buttons.allElementsBoundByIndex {
print(button.label)
}
その他Tips
便利だったので覚書程度に。
色やイメージのテストについて
色やイメージ自体は拾えないが、変更の挙動があったかどうかを
accessibilityIdentifierで拾うことでカバーしました。
// 色の設定のたびにaccessibilityIdentifierを付け直しています。
button.tintColor = isStar ? .systemYellow : systemGray
button.accessibilityIdentifier = isStar ? "star_button" : "notStar_button"
let isStar = app.buttons["star_button"].exists
キーボードのReturnKey(DoneKey)
extensionでまとめておくと便利でした。
extension XCUIApplication {
// 日本語と英語のキーボードのみ対応
var doneKey: XCUIElement {
// キーボードの表示アニメーションのラグを考慮して1秒探します。
if keyboards.buttons["完了"].waitForExistence(timeout: 1) {
return keyboards.buttons["完了"]
} else {
return keyboards.buttons["Done"]
}
}
var returnKey: XCUIElement {
// キーボードの表示アニメーションのラグを考慮して1秒探します。
if keyboards.buttons["改行"].waitForExistence(timeout: 1) {
return keyboards.buttons["改行"]
} else {
return keyboards.buttons["Return"]
}
}
}
キーボードの入力と強制タップ
こちらもextensionでまとめておくと便利でした。
強制タップはタップできないオブジェクトもタップしたい時に使います。
UIImagePickerController
で画像を選択するのに使いました。
extension XCUIElement {
// テキストに新しい文字列を入力する
func clearAndEnterText(text: String) {
guard let stringValue = self.value as? String else {
XCTFail("Tried to clear and enter text into a non string value")
return
}
self.tap()
let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count)
self.typeText(deleteString)
self.typeText(text)
}
// 強制タップ
func forceTap() {
if self.isHittable {
self.tap()
} else {
let coordinate: XCUICoordinate = self.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
coordinate.tap()
}
}
}
最後に
初めてUITestを実装してみましたが、意外と面白かったです。
慣れてくるといろんな挙動が表現できるようになるものの時間も結構かかりました。
つまり変更に強いテストを書く必要があります。
プロジェクトにマッチしたPageObjectパターンの設計や
accessibilityIdentifierをうまくまとめる方法など工夫がまだまだ必要だと感じました。
そもそも無理にUITestでテストする必要がないこともあるかもしれません。
ライブラリを使ったテストなども試して、ベストプラクティスを見つけようと思います。
参考にした記事や書籍
- iOSアプリ開発自動テストの教科書
- XCUITestのつらさを乗り越えて、iOSアプリにUITestを導入する
- Xcode7からのUI Testing&XCUIElementの基本操作まとめ
- Neither element nor any descendant has keyboard focusを対処する