AppiumでiOS/Androidアプリの自動テストを試した 〜Appiumをインストールして自作サンプルアプリをなんとか動かすまで〜

この記事は[Selenium/Appium Advent Calendar 2018](https://qiita.com/advent-calendar/2018/selenium_and_appium)の11日目の記事です。

Advent Calendarに初めて参加しました:confetti_ball:
どうぞ、よろしくお願いします:bangbang:

# 前置き
iOS/Androidアプリのテスト効率化を調査した過程でAppiumを知りました。意外に「インストール、設定、自作サンプルアプリ用意、テストコード実行」の一連の流れが揃っている情報は少ないと感じました。せっかくなので実際に動くところまでの手順をまとめました。筆者は一人で数日奮闘してしまいました:tired_face::tired_face::tired_face:
今後Appiumを始める方々の負担が少しでも無くなれば幸いです:smiley:

# 動作環境
| 　 | 　 |
|---|---|
|OS  |macOS Mojave 10.14.1|
| Homebrew  |1.8.4  |
|Node.js  |11.3.0  |
|Java |10.0.1  |
|Appium  |1.10.0  |
|Appium Desktop |1.9.0  |
|Ruby  |2.3.7p456  |

# 構成
[GitHub](https://github.com/devnokiyo/AppiumDemo)に公開しています。記事と併せてご覧ください。

```bash
.
├── Gemfile                             # Ruby+RSpecでテストを書くためのGemを記述
├── Gemfile.lock
├── SampleApp                           # テスト対象のアプリ
│   ├── AppiumDemo.app                   # iOS
│   └── app-debug.apk                    # Android
├── SampleAppSource                     # テスト対象のアプリのソースコード
│   ├── AppiumDemo_Android               # Android
│   └── AppiumDemo_iOS                   # iOS
└── spec                                # テストコード
    ├── test.rb                          # シナリオテストのコード
    └── spec_helper.rb                   # ヘルパーのコード
```


# 事前準備
1. Homebrewをインストールします。
[こちら](https://qiita.com/items/3c292218cbafc2898507#homebrew%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B)をご覧ください。但し、現在筆者の環境は以下の通りです。

    ```bash
    $ brew -v
    Homebrew 1.8.4
    ```
1. Node.jsをインストールします。

    ```bash
    $ brew install node
    $ node -v
    v11.3.0
    ```
1. bundlerをインストールします。

    ```bash
    $ gem install bundler
    $ bundler -v
    Bundler version 1.17.1
    ```

# Appiumをインストールする
1. 本体をインストールします。

    ```bash
    $ npm install -g appium
    $ appium -v
    1.10.0
    ```
1. 動作環境診断ツールをインストールします。

    ```bash
    $ npm install -g appium-doctor
    ```

# Appiumの利用可否を診断してNGを解消する
## まずは診断!!
```bash
$ appium-doctor
```
筆者の環境のNG項目はスクリーンショットのとおりです。残念ながら結構NGがありました:dizzy_face:
<img width="842" alt="appium_doctor_01.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/944f51fc-af08-272d-d758-d17bb808b136.png">
1. Xcode Command Line Toolsをインストールします。
https://developer.apple.com/download/more/
筆者環境ではMojave向けを選択します。
![Xcode Command Line Tools.png](https://qiita-image-store.s3.amazonaws.com/0/240974/b68622f1-5369-8696-e99d-9ec8208ea1f7.png)
## NGを解消する
1. Carthegeをインストールします。

    ```bash
    $ brew install carthage
    ```
1. ANDROID_HOMEを定義します。

    ```bash
    $ export ANDROID_HOME=$HOME/Library/Android/sdk
    ```
1. JAVA_HOMEを定義します。
筆者環境ではJava 10を利用します。

    ```bash
    $ export JAVA_HOME=`/usr/libexec/java_home -v 10`
    ```
1. パスに`$JAVA_HOME/bin`を通します。

    ```bash
    $ export PATH=$JAVA_HOME/bin:$PATH
    ```

環境変数は必要に応じて`.bash_profile`などへ設定してください。


## 再び診断
```bash
$ appium-doctor
```
問題なし:thumbsup:
<img width="842" alt="appium_doctor_02.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/ecdad7f3-a3ff-2961-92d4-c9a8b245d478.png">
## Appiumで必要なGemをインストール
筆者がRailsを学習中なのでRuby+RSpecでテストを書きました。先述の[構成](https://qiita.com/items/3cea66eb83280985d6cc#%E6%A7%8B%E6%88%90)のGemfileを`bundle install`します。


# テストを書く前に
この記事ではTDDではなくテスト対象のアプリが既にある状態でテストを書きます。
## サンプルアプリの概要
自動テストを簡単に検証するため「ログイン画面」と「ログイン成功後の画面(ようこその画面)」のみのサンプルアプリです。iOS/Androidでだいたい同じ物を用意しました。左側がiOS版、右側がAndroid版です。

1. ログイン画面
    ![01.png](https://qiita-image-store.s3.amazonaws.com/0/240974/be5d2118-bc93-952b-dd29-bcf6b38334bc.png)

1. ログイン認証エラー
    ![02.png](https://qiita-image-store.s3.amazonaws.com/0/240974/61f71098-b652-4324-1cfe-b4ee6a4ae660.png)

1. ログイン成功
    ![03.png](https://qiita-image-store.s3.amazonaws.com/0/240974/7ab7e0e3-1b84-bfe2-6baf-6a9a537828cd.png)

## 各UIコンポーネントの指定方法について
HTMLの要素のIDのようにiOS/AndroidのUIコンポーネントにIDを指定します。テストコードが共通化し易くなるので極力iOSとAndroidで同一IDにします。
### iOS
AccessibilityのIdentifierを設定します。
![04.png](https://qiita-image-store.s3.amazonaws.com/0/240974/2caeb962-c27e-427d-1942-ae06ab28e29c.png)
### Android
idを指定します。
![05.png](https://qiita-image-store.s3.amazonaws.com/0/240974/85e762da-2db6-7e07-7ca1-b7b589eeac73.png)
少し余談です。AndroidのコンポーネントのIDを命名するとき、スネークケースが圧倒的に多いと思います。筆者は[Kotlin Android Extensions](https://kotlinlang.org/docs/tutorials/android-plugin.html)を利用して開発しているのでAndroidのUIコンポーネントのIDをキャメルケースで命名しています。
### 今回テストコードで登場するUI
1. ログイン画面

    |UIコンポーネント|iOSの指定方法|Androidの指定方法|
    |---|---|---|
    |テキストボックス<br>ID|txtId|同左|
    |テキストボックス<br>パスワード|txtPassword|同左|
    |ログインボタン|btnLogin|同左|
    |認証エラーダイアログ<br>タイトル(※)|//XCUIElementTypeStaticText[@name="認証エラー"]|alertTitle|
    |認証エラーダイアログ<br>メッセージ(※)|IDまたはパスワードが違います。|message|
    |認証エラーダイアログ<br>OK(※)|OK|button2|
    ※ 認証ダイアログには明示的にIDを指定していません。IDの割出し方法は後述します。

1. ようこその画面

    |UIコンポーネント|iOSの指定方法|Androidの指定方法|
    |---|---|---|
    |ラベル<br>「ようこそ」|lblTitle|同左|

# テストコードについて
構文や仕様は公式や既に解説して下さっている情報がありますので、コメントで簡単に解説します。iOS/Androidの各テストを1つのテストコードで動かします。

```ruby:spec_helper.rb
require "rubygems"
require "appium_lib"

# iOSシミュレーター(iOS 12.1)の設定
ios_caps = {
  caps: {
    "platformName": "iOS",
    "platformVersion": "12.1",
    "deviceName": "iPhone Simulator",
    "automationName": "XCUITest",
    "app": "./SampleApp/AppiumDemo.app"
  },
  appium_lib: {
    wait: 10
  }
}

# Androidエミュレーター(Android 9.0)の設定
# 【補足】"platformVersion": "x.x"で指定するサンプルを多く見かけましたが、
# エミュレーターで実行するなら「"avd": "xxx"」が良いと思いました。
# この指定方法だとエミュレーターも自動で起動します。
android_caps = {
  caps: {
    "platformName": "Android",
#    "platformVersion": "8.1",
    "deviceName": "Android Emulator",
    "automationName": "Appium",
    "appPackage": "com.example.devnokiyo.appiumdemo",
    "app": "./SampleApp/app-debug.apk",
    "appActivity": "com.example.devnokiyo.appiumdemo.MainActivity",
    "avd": "Pixel_2_API_28"
  },
  appium_lib: {
    wait: 10
  }
}

RSpec.configure { |c|
  c.before(:each) {
    caps = ios? ? ios_caps : android_caps
    @driver = Appium::Driver.new(caps)
    @driver.start_driver
    Appium.promote_appium_methods Object
  }

  c.after(:each) {
    @driver.driver_quit
  }
}

# テスト対象がiOS/Androidか判定
#
# @return [true,false] iOSならtrue、Androidならfalse
def ios?
  return ENV["PLATFORM"] == "iOS"
end
```

```ruby:test.rb
require 'spec_helper'

describe "ログイン" do
  context '成功' do
    it "エラーダイアログが表示されないこと" do
      # 正しいID/パスワードでログインする
      operation_login('devnokiyo', 'abcd1234')
      # エラーダイアログが表示されないこと
      expect(dialog_title).to eq(nil)
    end
  end

  context '失敗' do
    before do
      # 不正なID/パスワードでログインする
      operation_login('devnokiyo', 'abcd123')
    end

    it 'エラーダイアログが表示されること' do
      # ダイアログのタイトルは「認証エラー」であること
      expect(dialog_title).to eq('認証エラー')
      # ダイアログのメッセージは「IDまたはパスワードが違います。」であること
      expect(dialog_message).to eq('IDまたはパスワードが違います。')
    end

    it '「OK」ボタンをタップするとエラーダイアログが消えること' do
      # ダイアログの「OK」ボタンをタップする
      tap_ok
      # ダイアログが非表示であること
      expect(dialog_title).to eq(nil)
    end
  end
end

describe 'ようこその画面' do
  context 'ログイン画面から遷移' do
    it "ログイン認証に成功すると、ようこその画面に遷移すること" do
      # 正しいID/パスワードでログインする
      operation_login('devnokiyo', 'abcd1234')
      # 「ようこそ」が表示されていること
      expect(label_title).to eq('ようこそ')
    end

    it "ログイン認証に失敗すると、ようこその画面に遷移しないこと" do
      operation_login('devnokiyo', 'abcd123')
      expect(label_title).to eq(nil)
    end
  end
end

# ログイン操作
#
# @param [String] id ID
# @param [String] password パスワード
def operation_login(id, password)
  find_element(:id, 'txtId').send_keys(id)
  find_element(:id, 'txtPassword').send_keys(password)
  find_element(:id, 'btnLogin').click
end

# ダイアログのタイトルを取得
#
# @return [String] ダイアログのタイトル
# @return [nil] ダイアログが表示されていない場合はnil
def dialog_title
  if ios?
    xpath = '//XCUIElementTypeStaticText[@name="認証エラー"]'
    return nil if find_elements(:xpath, xpath).empty?
    find_element(:xpath, xpath).value
  else
    id = 'alertTitle'
    return nil if find_elements(:id, id).empty?
    find_element(:id, id).text
  end
end

# ダイアログのメッセージを取得
#
# @return [String] ダイアログのメッセージ
# @return [nil] ダイアログが表示されていない場合はnil
def dialog_message
  if ios?
    id = 'IDまたはパスワードが違います。'
    return nil if find_elements(:id, id).empty?
    find_element(:id, id).value
  else
    id = 'message'
    return nil if find_elements(:id, id).empty?
    find_element(:id, id).text
  end
end

# 「OK」ボタンをタップ
#
def tap_ok
  if ios?
    find_element(:id, "OK").click
  else
    find_element(:id, "button2").click
  end
end

# ラベルのタイトルを取得
#
# @return [String] ラベルのタイトル
# @return [nil] ラベルが存在しない場合はnil
def label_title
  return nil if find_elements(:id, 'lblTitle').empty?
  find_element(:id, 'lblTitle').text
end
```

# 実行する！
では、動かしてみましょう:slight_smile:
繰返しになりますが[GitHub](https://github.com/devnokiyo/AppiumDemo)に公開しています。このリポジトリの直下に移動します。

```bash
$ pwd
/Users/devnokiyo/repos/appiumdemo

$ appium &
[1] 46107
[Appium] Welcome to Appium v1.10.0
[Appium] Appium REST http interface listener started on 0.0.0.0:4723
```
## iOS

```bash
$ PLATFORM=iOS rspec spec/test.rb
```
iOSシミュレーターが起動して自動テストが始まります。
![06.png](https://qiita-image-store.s3.amazonaws.com/0/240974/be44d11f-d62a-f804-4a6b-8e297ae5e3a4.png)

テストが完了すると以下のように結果が表示されます。5つのテストが全て成功していることがわかります。

```bash
Finished in 2 minutes 2.8 seconds (files took 0.66336 seconds to load)
5 examples, 0 failures
```

少し寄り道してテストの失敗も見てみましょう。
テストコードを以下のように変更します。

```ruby:test.rb
expect(label_title).to eq('よーこそ')
```

再度テストを実行します。

```bash
$ PLATFORM=iOS rspec spec/test.rb
    :
    :
Failures:

  1) ようこその画面 ログイン画面から遷移 ログイン認証に成功すると、ようこその画面に遷移すること
     Failure/Error: expect(label_title).to eq('よーこそ')
     
       expected: "よーこそ"
            got: "ようこそ"
     
       (compared using ==)
     # ./spec/test.rb:41:in `block (3 levels) in <top (required)>'

Finished in 2 minutes 1.8 seconds (files took 0.41319 seconds to load)
5 examples, 1 failure

Failed examples:

rspec ./spec/test.rb:37 # ようこその画面 ログイン画面から遷移 ログイン認証に成功すると、ようこその画面に遷移すること
```
普通に失敗しましたね:stuck_out_tongue_winking_eye:
続いてAndroidのテストを実行しますので`test.rb`のコードは元に戻してください。

## Android
`spec_helper.rb`のコメントにも書きましたが、capabilitiesの指定に`platformVersion`を利用しているサンプルが多くありました。また「エミュレーターが自動で起動してくれない」という情報もありました。`avd`で指定するとエミュレーターが自動で起動します。
実行してみます。ソースコードを見ればわかりますが、`PLATFORM=Android`と指定しなくても`PLATFORM=iOS`以外ならOKです。

```bash
$ PLATFORM=Android rspec spec/test.rb
```
Androidエミュレーターが起動して自動テストが始まります。
![07.png](https://qiita-image-store.s3.amazonaws.com/0/240974/7e9f126a-8a0a-efa7-3436-d4f2fb60ede3.png)

iOS同様にテストが完了すると以下のように結果が表示されます。5つのテストが全て成功していることがわかります。

```bash
Finished in 2 minutes 20.2 seconds (files took 0.49102 seconds to load)
5 examples, 0 failures
```

iOSと同じなので'よーこそ'のくだりは割愛します:bow:


本記事のタイトルのとおり、Appiumをインストールして自作サンプルアプリをなんとか動かすことができました。

# 【補足】分からないIDを割出す
[こちら](https://qiita.com/items/3cea66eb83280985d6cc#%E4%BB%8A%E5%9B%9E%E3%83%86%E3%82%B9%E3%83%88%E3%82%B3%E3%83%BC%E3%83%89%E3%81%A7%E7%99%BB%E5%A0%B4%E3%81%99%E3%82%8Bui)で言及した分からないIDを割出すためにAppium Desktopを使いました。ID割出し専門ツールではなくAppiumにGUIを提供するツールですが、今回はIDの割出しに使用します。

## 事前準備
1. ダウンロード/インストールする
[こちら](https://github.com/appium/appium-desktop/releases/tag/v1.9.0)からダウンロードしてインストールします。筆者は「Appium-1.9.0.dmg」にしました。

1. Appium Desktopとサーバーを起動する
    「Start Server...」ボタンを押下します。
    <img width="762" alt="スクリーンショット 2018-12-09 4.53.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/33e06241-b5b9-aa97-50f1-614be2859a55.png">
1. セッションを開始する
    赤枠の「Start Inspector Session」ボタンを押下します。
    <img width="762" alt="スクリーンショット 2018-12-09 4.58.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/a33baf52-af69-ee77-1882-227f0f2ea3e7.png">
    capabilitiesの指定画面が表示されます。
    <img width="1192" alt="スクリーンショット 2018-12-09 5.01.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/7731ee26-9f12-d035-3f90-1f319c5cfc7b.png">

## iOS
1. capabilitiesを設定する
    `spec_helper.rb`のcapabilitiesと同等の値を入力します。appのみフルパスに変更しました。
    <img width="1192" alt="スクリーンショット 2018-12-09 5.05.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/5e90fdb4-b7d1-ec02-0b55-31db66a1884a.png">
1. 「Start Session」ボタンを押下する
    シミュレーターと同時に構成要素を確認する画面が表示されます。
    <img width="505" alt="スクリーンショット 2018-12-09 5.11.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/f0e1b7c9-ffde-78ac-36e1-8898003f684b.png">
    <img width="1192" alt="スクリーンショット 2018-12-09 5.11.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/3691a7a6-9ed5-7df0-fc96-636d860599d3.png">
1. 認証エラーダイアログのIDを取得する
    1. 認証エラーダイアログを出す  
        シミュレーターで不正なID/パスワードを入力して認証エラーダイアログを出します。
        <img width="505" alt="スクリーンショット 2018-12-09 5.20.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/e2d29c8b-388a-ba77-f0a3-63d36ab886ec.png">
    1. タイトルのID取得  
        画面上側の赤枠「リフレッッシュ」ボタンを押下すると、ダイアログが表示されている状態に切り替わります。その状態でダイアログのタイトル部分「認証エラー」をクリックします。すると右側の「Selectd Element」に取得方法が表示されます。xpathでないと取得できないようです。
        <img width="1192" alt="スクリーンショット 2018-12-09 5.22.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/d298e94e-8779-eedf-ec10-52a77bdbf910.png">
    1. メッセージのID取得  
        続いてダイアログのメッセージ部分「IDまたは...」をクリックします。こちらはIDで取得できます。
        <img width="1192" alt="スクリーンショット 2018-12-09 5.22.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/6faacec0-2c3e-1393-fce8-b21ef1a23178.png">
    1. 「OK」ボタンのID取得  
    さらに続いてダイアログの「OK」ボタンをクリックします。こちらもIDで取得できます。
        <img width="1192" alt="スクリーンショット 2018-12-09 5.22.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/5bdc24ce-fedf-f4a8-8a31-9291bf2cb6ac.png">

## Android
iOSと同様なので手順を少し省略します。

1. capabilitiesを設定する
    `spec_helper.rb`のcapabilitiesと同等の値を入力します。appのみフルパスに変更しました。
    <img width="1192" alt="スクリーンショット 2018-12-09 6.02.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/7db47bca-e1c1-69d6-36f6-dedfb887fb59.png">
1. 「Start Session」ボタンを押下する
    エミュレーターと同時に構成要素を確認する画面が表示されます。
    <img width="379" alt="スクリーンショット 2018-12-09 6.06.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/a9a0fad4-3e3f-53bb-fdb3-f01d2f758cea.png">
    <img width="1192" alt="スクリーンショット 2018-12-09 6.07.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/f705c6ee-4d00-55e2-fe6e-e415803be6f4.png">


1. 認証エラーダイアログのIDを取得する
    1. 認証エラーダイアログを出す  
        エミュレーターで不正なID/パスワードを入力して認証エラーダイアログを出します。  
        <img width="379" alt="スクリーンショット 2018-12-09 6.09.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/3b42022e-a482-c228-989d-1cea6071ea1a.png">
    1. タイトルのID取得  
        「リフレッッシュ」ボタンを押下してダイアログのタイトル部分「認証エラー」をクリックしてIDを取得します。
        <img width="1192" alt="スクリーンショット 2018-12-09 6.11.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/711e0bc1-4b9d-829d-e7c0-141dea4d5206.png">

    1. メッセージのID取得  
        続いてダイアログのメッセージ部分「IDまたは...」をクリックしてIDを取得します。
        <img width="1192" alt="スクリーンショット 2018-12-09 6.13.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/7b9e9fd8-2af0-1b03-12c3-8b7afc66f6e0.png">

    1. 「OK」ボタンのID取得  
        さらに続いてダイアログの「OK」ボタンをクリックしてIDを取得します。
        <img width="1192" alt="スクリーンショット 2018-12-09 6.15.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/240974/838368c2-8179-44fb-6a98-86b40f363413.png">

# 終わりに
自動テストの経験が少ないこともあり数日掛けて、ようやくここまで来ました。Railsを学習中なのでRuby+RSpecにも触れることができて良かったです。また、記事とは関係ありませんが、Advent Calendarに参加できて良かったです。なんだか少し勇気がいりました:sweat_smile::sweat_smile: