この記事はSelenium/Appium Advent Calendar 2018の11日目の記事です。
Advent Calendarに初めて参加しました
どうぞ、よろしくお願いします
前置き
iOS/Androidアプリのテスト効率化を調査した過程でAppiumを知りました。意外に「インストール、設定、自作サンプルアプリ用意、テストコード実行」の一連の流れが揃っている情報は少ないと感じました。せっかくなので実際に動くところまでの手順をまとめました。筆者は一人で数日奮闘してしまいました
今後Appiumを始める方々の負担が少しでも無くなれば幸いです
動作環境
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 # ヘルパーのコード
事前準備
-
Homebrewをインストールします。
こちらをご覧ください。但し、現在筆者の環境は以下の通りです。$ brew -v Homebrew 1.8.4
-
Node.jsをインストールします。
$ brew install node $ node -v v11.3.0
-
bundlerをインストールします。
$ gem install bundler $ bundler -v Bundler version 1.17.1
Appiumをインストールする
-
本体をインストールします。
$ npm install -g appium $ appium -v 1.10.0
-
動作環境診断ツールをインストールします。
$ npm install -g appium-doctor
Appiumの利用可否を診断してNGを解消する
まずは診断!!
$ appium-doctor
筆者の環境のNG項目はスクリーンショットのとおりです。残念ながら結構NGがありました
- Xcode Command Line Toolsをインストールします。
https://developer.apple.com/download/more/
筆者環境ではMojave向けを選択します。
NGを解消する
-
Carthegeをインストールします。
$ brew install carthage
-
ANDROID_HOMEを定義します。
$ export ANDROID_HOME=$HOME/Library/Android/sdk
-
JAVA_HOMEを定義します。
筆者環境ではJava 10を利用します。$ export JAVA_HOME=`/usr/libexec/java_home -v 10`
-
パスに
$JAVA_HOME/bin
を通します。$ export PATH=$JAVA_HOME/bin:$PATH
環境変数は必要に応じて.bash_profile
などへ設定してください。
再び診断
$ appium-doctor
Appiumで必要なGemをインストール
筆者がRailsを学習中なのでRuby+RSpecでテストを書きました。先述の構成のGemfileをbundle install
します。
テストを書く前に
この記事ではTDDではなくテスト対象のアプリが既にある状態でテストを書きます。
サンプルアプリの概要
自動テストを簡単に検証するため「ログイン画面」と「ログイン成功後の画面(ようこその画面)」のみのサンプルアプリです。iOS/Androidでだいたい同じ物を用意しました。左側がiOS版、右側がAndroid版です。
各UIコンポーネントの指定方法について
HTMLの要素のIDのようにiOS/AndroidのUIコンポーネントにIDを指定します。テストコードが共通化し易くなるので極力iOSとAndroidで同一IDにします。
iOS
AccessibilityのIdentifierを設定します。
Android
idを指定します。
少し余談です。AndroidのコンポーネントのIDを命名するとき、スネークケースが圧倒的に多いと思います。筆者はKotlin Android Extensionsを利用して開発しているのでAndroidのUIコンポーネントのIDをキャメルケースで命名しています。
今回テストコードで登場するUI
-
ログイン画面
UIコンポーネント iOSの指定方法 Androidの指定方法 テキストボックス
IDtxtId 同左 テキストボックス
パスワードtxtPassword 同左 ログインボタン btnLogin 同左 認証エラーダイアログ
タイトル(※)//XCUIElementTypeStaticText[@name="認証エラー"] alertTitle 認証エラーダイアログ
メッセージ(※)IDまたはパスワードが違います。 message 認証エラーダイアログ
OK(※)OK button2 ※ 認証ダイアログには明示的にIDを指定していません。IDの割出し方法は後述します。 -
ようこその画面
UIコンポーネント iOSの指定方法 Androidの指定方法 ラベル
「ようこそ」lblTitle 同左
テストコードについて
構文や仕様は公式や既に解説して下さっている情報がありますので、コメントで簡単に解説します。iOS/Androidの各テストを1つのテストコードで動かします。
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
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
実行する!
では、動かしてみましょう
繰返しになりますが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
テストが完了すると以下のように結果が表示されます。5つのテストが全て成功していることがわかります。
Finished in 2 minutes 2.8 seconds (files took 0.66336 seconds to load)
5 examples, 0 failures
少し寄り道してテストの失敗も見てみましょう。
テストコードを以下のように変更します。
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 # ようこその画面 ログイン画面から遷移 ログイン認証に成功すると、ようこその画面に遷移すること
普通に失敗しましたね
続いてAndroidのテストを実行しますのでtest.rb
のコードは元に戻してください。
Android
spec_helper.rb
のコメントにも書きましたが、capabilitiesの指定にplatformVersion
を利用しているサンプルが多くありました。また「エミュレーターが自動で起動してくれない」という情報もありました。avd
で指定するとエミュレーターが自動で起動します。
実行してみます。ソースコードを見ればわかりますが、PLATFORM=Android
と指定しなくてもPLATFORM=iOS
以外ならOKです。
$ PLATFORM=Android rspec spec/test.rb
Androidエミュレーターが起動して自動テストが始まります。
iOS同様にテストが完了すると以下のように結果が表示されます。5つのテストが全て成功していることがわかります。
Finished in 2 minutes 20.2 seconds (files took 0.49102 seconds to load)
5 examples, 0 failures
iOSと同じなので'よーこそ'のくだりは割愛します
本記事のタイトルのとおり、Appiumをインストールして自作サンプルアプリをなんとか動かすことができました。
【補足】分からないIDを割出す
こちらで言及した分からないIDを割出すためにAppium Desktopを使いました。ID割出し専門ツールではなくAppiumにGUIを提供するツールですが、今回はIDの割出しに使用します。
事前準備
-
ダウンロード/インストールする
こちらからダウンロードしてインストールします。筆者は「Appium-1.9.0.dmg」にしました。 -
セッションを開始する
赤枠の「Start Inspector Session」ボタンを押下します。
capabilitiesの指定画面が表示されます。
iOS
- capabilitiesを設定する
spec_helper.rb
のcapabilitiesと同等の値を入力します。appのみフルパスに変更しました。
- 「Start Session」ボタンを押下する
シミュレーターと同時に構成要素を確認する画面が表示されます。
- 認証エラーダイアログのIDを取得する
- 認証エラーダイアログを出す
シミュレーターで不正なID/パスワードを入力して認証エラーダイアログを出します。
- タイトルのID取得
画面上側の赤枠「リフレッッシュ」ボタンを押下すると、ダイアログが表示されている状態に切り替わります。その状態でダイアログのタイトル部分「認証エラー」をクリックします。すると右側の「Selectd Element」に取得方法が表示されます。xpathでないと取得できないようです。
- メッセージのID取得
続いてダイアログのメッセージ部分「IDまたは...」をクリックします。こちらはIDで取得できます。
- 「OK」ボタンのID取得
さらに続いてダイアログの「OK」ボタンをクリックします。こちらもIDで取得できます。
- 認証エラーダイアログを出す
Android
iOSと同様なので手順を少し省略します。
終わりに
自動テストの経験が少ないこともあり数日掛けて、ようやくここまで来ました。Railsを学習中なのでRuby+RSpecにも触れることができて良かったです。また、記事とは関係ありませんが、Advent Calendarに参加できて良かったです。なんだか少し勇気がいりました