XCUIElementの使い方をざっくりまとめてみた

# XCUITest周りで時間を浪費しないために

[XCUIElementTypeQueryProvider](https://developer.apple.com/documentation/xctest/xcuielementtypequeryprovider)の定義済みクエリを使うことで、テストで利用するUIを特定することができます。
しかし、まとまった記事が少なく扱い方が見つからず時間を浪費したものもいくつかあったので自分で発見したやり方も含めて簡単にまとめてみます。

---

※下記を省略してます

```swift:SampleViewUITest.swift
let app = XCUIApplication()
```

---

# AccessibilityIdentifierを使えるパターン

### UIView

```swift:SampleViewController.swift
view.accessibilityIdentifier = "sample_view"
```
```swift:SampleViewUITest.swift
let view = app.otherElements["sample_view"]
```

---

### UIButton
```swift:SampleViewController.swift
let button = UIButton()
button.accessibilityIdentifier = "sample_button"

// サンプルというタイトルをセット
button.setTitle("サンプル", for: .normal)
```


```swift:SampleViewUITest.swift
let button = app.buttons["sample_button"]

// ボタンのタイトルを確認
XCTAssertEqual(button.label, "サンプル") 
```

---

### UILabel
```swift:SampleViewController.swift
let label = UILabel()
label.accessibilityIdentifier = "sample_label"

// サンプルというテキストをセット
label.text = "サンプル"
```
```swift:SampleViewUITest.swift
let label = app.staticTexts["sample_label"]

// ラベルのテキストを確認
XCTAssertEqual(label.label, "サンプル") 
```

---

### UITextField (SecureTextEntryではない）
テキストの中身は```.value as! String```でキャストして使います。

```swift:SampleViewController.swift
let textField = UITextField()
textField.accessibilityIdentifier = "sample_textField"

// サンプルというテキストをセット
textField.text = "サンプル"
```
```swift:SampleViewUITest.swift
let textField = app.textFields["sample_textField"] 

// ラベルのテキストを確認
XCTAssertEqual(textField.value as! String, "サンプル") 
```

---

### UITextField (SecureTextEntry）
テキストの中身は```.value as! String```でキャストして使うと
中身が```"・・・・"```になるのでバリデーションチェックなどには使えません。
※いい方法あれば教えて欲しいです。

```swift:SampleViewController.swift
let textField = UITextField()
textField.isSecureTextEntry = true
textField.accessibilityIdentifier = "sample_textField"

// サンプルというテキストをセット
textField.text = "サンプル"
```
```swift:SampleViewUITest.swift
let textField = app.secureTextFields["sample_textField"] 

// ラベルのテキストを確認
XCTAssertEqual(textField.value as! String, "サンプル") // failure
```

---

### UITabBarとUITabBarItem

```swift:SampleViewController.swift
tabBarController?.tabBar.accessibilityIdentifier = "sample_tabBar"
tabBarItem.accessibilityIdentifier = "sample_barItem"
```
```swift:SampleViewUITest.swift
let tabBar = app.tabBars["sample_tabBar"]
let barItem = tabBar.buttons["sample_barItem"]
```

---

### UIImageView
```.isAccessibilityElement = true```
これが無いと使えません。

```swift:SampleViewController.swift
let imageView = UIImageView()
imageView.isAccessibilityElement = true
imageView.accessibilityIdentifier = "sample_image"
```
```swift:SampleViewUITest.swift
let tabBar = app.tabBars["sample_tabBar"]
let barItem = tabBar.buttons["sample_barItem"]
```



---
# AccessibilityIdentifierを使えないパターン

### SwipeMenu
``` .accessibilityIdentifier ```
が設定できないので

``` .accessibilityLabel ```
を設定しておくとうまく拾える。


```swift:SampleViewController.swift
let action = UIContextualAction()
action.accessibilityLabel = "leadingSwipe_action"
```
```swift:SampleViewUITest.swift
let action = app.tables.cells.firstMatch.buttons["leadingSwipe_action"]
```

---

### AlertとAlertAction

ラベル名でしか拾えない。※いい方法あれば誰か教えて欲しいです。

```swift:SampleViewController.swift
let alert = UIAlertController()
alert.view.accessibilityIdentifier = "alert_view"
let cancelAction = UIAlertAction(
                    title: "キャンセル",
                    style: UIAlertAction.Style.cancel,
                    handler: nil))
// accessibilityIdentifierを指定しても使えない。
cancelAction.accessibilityIdentifier = "alert_cancel_button"
...
```
```swift:SampleViewUITest.swift
let alert = app.alerts["alert_view"]
// accessibilityIdentifierを指定しても使えない。
// let cancelAction = alert.buttons["alert_cancel_button"]

// ラベル名で拾える
let cancelAction = alert.buttons["キャンセル"]
```

---

## それでも拾い方がわからない時
例えば``` UISegmentedControl ```内のSFシンボルを使ったボタン

### 順番で拾うパターン

```swift:SampleViewUITest.swift
let segmentedControl = app.segmentedControls["sample_segmentedControl"]
let button01 = segmentedControl.buttons.element(boundBy: 0) // 拾いたいボタンのindex
```

###  順番が使えないパターン（動的に変化するなど）

SFシンボルの特有のラベル名を事前に調べておきます。
※SFシンボル名とは若干異なるようです。

```swift:SampleViewUITest.swift
let segmentedControl = app.segmentedControls["sample_segmentedControl"]
for button in segmentedControl.buttons.allElementsBoundByIndex {
    print(button.label)
}
```

---

# その他Tips
便利だったので覚書程度に。

### 色やイメージのテストについて
色やイメージ自体は拾えないが、変更の挙動があったかどうかを
accessibilityIdentifierで拾うことでカバーしました。

```swift:SampleViewController.swift
// 色の設定のたびにaccessibilityIdentifierを付け直しています。
button.tintColor = isStar ? .systemYellow : systemGray
button.accessibilityIdentifier = isStar ? "star_button" : "notStar_button"
```

```swift:SampleViewUITest.swift
let isStar = app.buttons["star_button"].exists
```

---

### キーボードのReturnKey(DoneKey)
extensionでまとめておくと便利でした。

```swift:extension_XCUIApplication.swift
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 ``` で画像を選択するのに使いました。

```swift:extension_XCUIElement.swift
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アプリ開発自動テストの教科書](https://www.amazon.co.jp/dp/B07TF46F7T)
- [XCUITestのつらさを乗り越えて、iOSアプリにUITestを導入する](https://speakerdeck.com/satotakeshi/xcuitestfalseturasawocheng-riyue-ete-iosahuriniuitestwodao-ru-suru)
- [Xcode7からのUI Testing&XCUIElementの基本操作まとめ](https://labs.septeni.co.jp/entry/2015/11/27/092534)
- [Neither element nor any descendant has keyboard focusを対処する
](https://blog.mothule.com/ios/test/ios-xctest-failed-for-hard-keyboard)


---