appiumのiOS用テスト環境を構築する

ネイティブアプリ向けのUIテスト環境であるappiumをiOS環境へ導入しようしたところ、iOS向け環境構築のまとまったドキュメントがなかったため、ここにまとめます。

# 前提
前提として、iOSのサンプルアプリ程度を作成できる程度の知識・スキルを読者に要求します。ご容赦ください。

# appiumとは
appiumは、WebDriverを使用してAndroidまたはiOSでネイティブアプリケーション、モバイルWebアプリケーション、およびハイブリッドアプリケーションをテストするためのオープンソース自動化ツールです。
仕組みについては、[Appiumの仕組みと使い方](https://qiita.com/k5n/items/899cf40a0021a6a92efd)や[Appium 2.0 ではじめるモバイルアプリテスト](https://www.slideshare.net/MasayukiWakizaka/appium-20)をご参考ください。

# 実行環境
- macOS 12.0.1
- XCode 13.3.1

## 環境構築手順
macOSやXcodeといったiOSアプリ開発に必須の環境構築については、ここでは省略します。

### 1. サンプルアプリの準備
次にXCodeでiOSのサンプルアプリを準備します。
アプリ名はご自身で適当につけて構いません。
ただし、テストコードを作成する際に`Bundle Identifier`が必要となるため、ここでメモします。

UIはStoryBoardを選択します。

プロジェクトを作成したら、以下のコードを`ViewController.swift`にコピー&ペーストします。

```swift
import UIKit

class ViewController: UIViewController {
    private let textFieldSize = CGSize(width: 200, height: 100)
    private let textField: UITextField = {
        let field = UITextField()
        field.borderStyle = .bezel
        
//         appiumのクライアントがUIを識別するために
//         Accessibility Identifierを設定します
        field.accessibilityIdentifier = "textField"
        return field
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        self.view.addSubview(self.textField)
    }
    
    override func viewWillLayoutSubviews() {
        let x = self.view.frame.midX - self.textFieldSize.width/2
        let y = self.view.frame.midY - self.textFieldSize.height/2
        self.textField.frame = CGRect(origin: CGPoint(x: x, y: y), size: self.textFieldSize)
    }
}
```

ビルドします。
ビルドのターゲットは、筆者は`15.4`、`iPhone 11`を選択しました。
ビルドし、アプリを立ち上げると以下の画面になります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208214/ccfdc17f-b893-46f8-4502-2de2609af210.png" width=50%>

### 2. nodeのインストール
nodeがインストールされていない場合は、brewでnodeをインストールします
```sh
brew install node
```

### 3. appium（サーバサイド）のセットアップ
appiumのサーバ実行環境にはCLI版とGUI版が存在します。CLIの操作に慣れていない方はGUI版をおすすめします。GUI版は[ここ](https://github.com/appium/appium-desktop/releases/tag/v1.22.3-4)から、もしくはbrewでインストールできます。
ここでは、CLI版のインストールについて記述します。

[appiumの公式ドキュメント](https://github.com/appium/appium/blob/master/docs/en/about-appium/getting-started.md)にしたがって、appiumをインストールします。

```sh
npm install -g appium
```

次にappiumの依存関係を検証するappium-doctorをインストールします
```sh
npm install -g appium-doctor
```

appium-doctorを実行し、環境を検証します。

```sh
appium-doctor --ios
```

依存関係が解消できていない場合は、appium-doctorの出力に従って、必要なアプリケーションをインストールします。
筆者の環境では、carthageというライブラリ管理ソフトウェアが足りないとのことなので、brewでcarthageをインストールしました

```sh
brew install carthage
```

### 4. appium（クライアント側）の準備
appiumのサーバは、WebDriverの標準プロトコルに対応しているため、javaやruby、javascript、pythonといったさまざまな言語に対応しています。
ここでは、python3のクライアントについて説明します。

まず、適当なディレクトリと`main.py`作成します
```sh
mkdir appium-client
cd appium-client
touch main.py
```

次に、pip3でappiumのpython3クライアントをインストールします
```
pip3 install Appium-Python-Client
```

### 5. テストの作成
クライアントをインストールしたら、具体的なテストの作成に移ります。
テストの完成形は次のようになります。
次のコードを`main.py`にコピー＆ペーストします。
ペーストしたら、bundleIdをStep1で作成したプロジェクトのbundler

```py
import os
import sys
import unittest
import socket

from time import sleep
from appium import webdriver

class AppiumTest(unittest.TestCase):
    def setUp(self):
        options = {
            'logLevel': 'info',
            'platformName': 'iOS',
            'deviceName': 'iPhone 11',
            'platformVersion': '15.4',
            'automationName': 'XCUITest',
            'bundleId': 'own.SampleApp', # ここを編集
        }
        
        protocol = 'http'
        host = socket.gethostname()
        ipaddress = socket.gethostbyname(host)
        port = '8100'
        path = '/wd/hub'
        self.driver = webdriver.Remote(f'{protocol}://{ipaddress}:{port}{path}', options)

    def tearDown(self):
        self.driver.quit()

    def test_textfield(self):
        element = self.driver.find_element_by_accessibility_id("textField")

        # 値を取得
        value = element.get_attribute('value')
        self.assertEqual(value, None)

        # テキストを入力
        word = "Hello, World!"
        element.send_keys(word)

        # 値を取得
        value = element.get_attribute('value')
        self.assertEqual(value, word)

if __name__ == '__main__':
    # テストを実行
    suite = unittest.TestLoader().loadTestsFromTestCase(AppiumTest)
    unittest.TextTestRunner(verbosity=2).run(suite)

```

### 6. テストの実行
テストが作成できたので、テストを実行します。
テストを実行するためにまず、iOS Simulatorを立ち上げます。

```sh
open -a Simulator.app
```

次にappiumのサーバを立ち上げます。
ローカルIPアドレスを取得するために、python3をコマンドライン実行しています。

```sh
appium --address `python3 -c "import socket; print(socket.gethostbyname(socket.gethostname()))"` --port 8100
```

共に立ち上がったら、最後にテストを実行します

```py
python3 main.py
```

出力は次のようになるはずです。（Deprecatedはここでは無視しています）

```sh
test_textfield (__main__.AppiumTest) ... [Deprecated] Please use 'find_element' with 'AppiumBy.ACCESSIBILITY_ID' instead.
ok

----------------------------------------------------------------------
Ran 1 test in 4.234s

OK
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208214/4cea3b66-77ea-a475-bcde-b650a8e9c2d4.gif" width=50%>

以上です。


# 参考文献
- [Appiumの仕組みと使い方](https://qiita.com/k5n/items/899cf40a0021a6a92efd)
- [Appium 2.0 ではじめるモバイルアプリテスト](https://www.slideshare.net/MasayukiWakizaka/appium-20)
- [appium Gatting Started](https://github.com/appium/appium/blob/master/docs/en/about-appium/getting-started.md)
- [Appium Desired Capabilities](https://appium.io/docs/en/writing-running-appium/caps/)
- [webdriver.io options](https://webdriver.io/docs/options/)