Edited at

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

この記事はSelenium/Appium Advent Calendar 2018の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に公開しています。記事と併せてご覧ください。

.

├── 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をインストールします。

    こちらをご覧ください。但し、現在筆者の環境は以下の通りです。

    $ brew -v
    
    Homebrew 1.8.4



  2. Node.jsをインストールします。

    $ brew install node
    
    $ node -v
    v11.3.0



  3. bundlerをインストールします。

    $ gem install bundler
    
    $ bundler -v
    Bundler version 1.17.1



Appiumをインストールする



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

    $ npm install -g appium
    
    $ appium -v
    1.10.0



  2. 動作環境診断ツールをインストールします。

    $ npm install -g appium-doctor
    



Appiumの利用可否を診断してNGを解消する


まずは診断!!

$ appium-doctor

筆者の環境のNG項目はスクリーンショットのとおりです。残念ながら結構NGがありました:dizzy_face:

appium_doctor_01.png

1. Xcode Command Line Toolsをインストールします。

https://developer.apple.com/download/more/

筆者環境ではMojave向けを選択します。

Xcode Command Line Tools.png


NGを解消する



  1. Carthegeをインストールします。

    $ brew install carthage
    



  2. ANDROID_HOMEを定義します。

    $ export ANDROID_HOME=$HOME/Library/Android/sdk
    



  3. JAVA_HOMEを定義します。

    筆者環境ではJava 10を利用します。

    $ export JAVA_HOME=`/usr/libexec/java_home -v 10`
    



  4. パスに$JAVA_HOME/binを通します。

    $ export PATH=$JAVA_HOME/bin:$PATH
    


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


再び診断

$ appium-doctor

問題なし:thumbsup:

appium_doctor_02.png


Appiumで必要なGemをインストール

筆者がRailsを学習中なのでRuby+RSpecでテストを書きました。先述の構成のGemfileをbundle installします。


テストを書く前に

この記事ではTDDではなくテスト対象のアプリが既にある状態でテストを書きます。


サンプルアプリの概要

自動テストを簡単に検証するため「ログイン画面」と「ログイン成功後の画面(ようこその画面)」のみのサンプルアプリです。iOS/Androidでだいたい同じ物を用意しました。左側がiOS版、右側がAndroid版です。


  1. ログイン画面

    01.png


  2. ログイン認証エラー

    02.png


  3. ログイン成功

    03.png



各UIコンポーネントの指定方法について

HTMLの要素のIDのようにiOS/AndroidのUIコンポーネントにIDを指定します。テストコードが共通化し易くなるので極力iOSとAndroidで同一IDにします。


iOS

AccessibilityのIdentifierを設定します。

04.png


Android

idを指定します。

05.png

少し余談です。AndroidのコンポーネントのIDを命名するとき、スネークケースが圧倒的に多いと思います。筆者はKotlin Android Extensionsを利用して開発しているのでAndroidのUIコンポーネントのIDをキャメルケースで命名しています。


今回テストコードで登場するUI



  1. ログイン画面

    UIコンポーネント
    iOSの指定方法
    Androidの指定方法

    テキストボックス
    ID
    txtId
    同左

    テキストボックス
    パスワード
    txtPassword
    同左

    ログインボタン
    btnLogin
    同左

    認証エラーダイアログ
    タイトル(※)
    //XCUIElementTypeStaticText[@name="認証エラー"]
    alertTitle

    認証エラーダイアログ
    メッセージ(※)
    IDまたはパスワードが違います。
    message

    認証エラーダイアログ
    OK(※)
    OK
    button2

    ※ 認証ダイアログには明示的にIDを指定していません。IDの割出し方法は後述します。




  2. ようこその画面

    UIコンポーネント
    iOSの指定方法
    Androidの指定方法

    ラベル
    「ようこそ」
    lblTitle
    同左



テストコードについて

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


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



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に公開しています。このリポジトリの直下に移動します。

$ 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

$ PLATFORM=iOS rspec spec/test.rb

iOSシミュレーターが起動して自動テストが始まります。

06.png

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

Finished in 2 minutes 2.8 seconds (files took 0.66336 seconds to load)

5 examples, 0 failures

少し寄り道してテストの失敗も見てみましょう。

テストコードを以下のように変更します。


test.rb

expect(label_title).to eq('よーこそ')


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

$ 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です。

$ PLATFORM=Android rspec spec/test.rb

Androidエミュレーターが起動して自動テストが始まります。

07.png

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

Finished in 2 minutes 20.2 seconds (files took 0.49102 seconds to load)

5 examples, 0 failures

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

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


【補足】分からないIDを割出す

こちらで言及した分からないIDを割出すためにAppium Desktopを使いました。ID割出し専門ツールではなくAppiumにGUIを提供するツールですが、今回はIDの割出しに使用します。


事前準備


  1. ダウンロード/インストールする

    こちらからダウンロードしてインストールします。筆者は「Appium-1.9.0.dmg」にしました。


  2. Appium Desktopとサーバーを起動する

    「Start Server...」ボタンを押下します。

    スクリーンショット 2018-12-09 4.53.04.png


  3. セッションを開始する

    赤枠の「Start Inspector Session」ボタンを押下します。

    スクリーンショット 2018-12-09 4.58.12.png

    capabilitiesの指定画面が表示されます。

    スクリーンショット 2018-12-09 5.01.37.png



iOS


  1. capabilitiesを設定する

    spec_helper.rbのcapabilitiesと同等の値を入力します。appのみフルパスに変更しました。

    スクリーンショット 2018-12-09 5.05.18.png


  2. 「Start Session」ボタンを押下する

    シミュレーターと同時に構成要素を確認する画面が表示されます。

    スクリーンショット 2018-12-09 5.11.34.png

    スクリーンショット 2018-12-09 5.11.17.png



  3. 認証エラーダイアログのIDを取得する


    1. 認証エラーダイアログを出す

      シミュレーターで不正なID/パスワードを入力して認証エラーダイアログを出します。
      スクリーンショット 2018-12-09 5.20.50.png

    2. タイトルのID取得

      画面上側の赤枠「リフレッッシュ」ボタンを押下すると、ダイアログが表示されている状態に切り替わります。その状態でダイアログのタイトル部分「認証エラー」をクリックします。すると右側の「Selectd Element」に取得方法が表示されます。xpathでないと取得できないようです。
      スクリーンショット 2018-12-09 5.22.17.png

    3. メッセージのID取得

      続いてダイアログのメッセージ部分「IDまたは...」をクリックします。こちらはIDで取得できます。
      スクリーンショット 2018-12-09 5.22.27.png

    4. 「OK」ボタンのID取得

      さらに続いてダイアログの「OK」ボタンをクリックします。こちらもIDで取得できます。
      スクリーンショット 2018-12-09 5.22.33.png




Android

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


  1. capabilitiesを設定する

    spec_helper.rbのcapabilitiesと同等の値を入力します。appのみフルパスに変更しました。

    スクリーンショット 2018-12-09 6.02.49.png


  2. 「Start Session」ボタンを押下する

    エミュレーターと同時に構成要素を確認する画面が表示されます。

    スクリーンショット 2018-12-09 6.06.28.png

    スクリーンショット 2018-12-09 6.07.43.png



  3. 認証エラーダイアログのIDを取得する


    1. 認証エラーダイアログを出す

      エミュレーターで不正なID/パスワードを入力して認証エラーダイアログを出します。

      スクリーンショット 2018-12-09 6.09.16.png

    2. タイトルのID取得

      「リフレッッシュ」ボタンを押下してダイアログのタイトル部分「認証エラー」をクリックしてIDを取得します。
      スクリーンショット 2018-12-09 6.11.26.png

    3. メッセージのID取得

      続いてダイアログのメッセージ部分「IDまたは...」をクリックしてIDを取得します。
      スクリーンショット 2018-12-09 6.13.17.png

    4. 「OK」ボタンのID取得

      さらに続いてダイアログの「OK」ボタンをクリックしてIDを取得します。
      スクリーンショット 2018-12-09 6.15.02.png




終わりに

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