AppiumによるUIテスト自動化
本記事はマイネット Advent Calendar15日目の記事です。
今回はネイティブ周りを担当している山木がAppiumによるUIテスト自動化についてお送りします。
Appiumの特徴
- WebView、フルネイティブ、Unity、cocos2d-x 可能
- iOS、Android 両方利用可能(ただしテストコードの共有はできない)
- 実機、シミュレーターでの実行 可能(Androidエミュレーター Genymotionも可)
- コマンドからの実行可能
- スクリーンショット可能(一部言語制限)
- テストコードは C#、Ruby、Objective-C、Java、node.js、Python
- 操作を記録しコード出力する機能もある
- ネイティブコードにテストコード、SDKを仕込むこと無くテストが可能
- デバッグビルドした ipa、apk があれば第三者によるテストコード作成、実施が可能
- 成果物は吐き出してくれないので自前実装作成が必要
- スクリーンショットでの画像比較も自前で実装すれば可能
- 後述にもあるが UIAutomation/UIAutomator を内部で利用している
- Gitにあがっているクライアントが最新であれば...GUIなどのカスタマイズも可能(現状古い)
公式
イメージ

事前にやったこと
- Xcode インストール(ver7.1)
- Xcode Command Line Tool インストール
- Appium.app クライアントのダウンロード(ver1.4.13)
- 必要ライブラリなどのインストール
$ brew install node $ npm install -g appium $ npm install wd $ npm install -g mocha $ npm install chai $ npm install chai-as-promised $ npm install colors $ brew install maven $ authorize_ios $ brew install imagemagick <- 画像比較
- Selenium.framework を "/Library/Frameworks" に配置(後述あり)
Androidの場合に必要
- JAVA インストール
- Android SDK インストール
- bash_profile (java, android SDK パス記述)
bash_profile(例)
export JAVA_HOME='/usr/libexec/java_home' export ANDROID_HOME='/Users/[user]/Library/Android/sdk'
- GenyMotionエミュレータ(android sdkのエミュレータは遅いので必要であれば...)
- 参考:http://qiita.com/snaka/items/9e770f2b8be2a6b2e662
- GenyMotionのアカウント作る
- GenyMotion > Shop > Free でクライアントダウンロード
- GenyMotionインストール
- Oracle VM VirtualBox インストール
- GenyMotion エミュレータ に GooglePalyService インストール
- 参考:http://nelog.jp/how-to-use-google-play-in-genymotion
- GooglePlay アカウントが必要(本番)
- エミュレーターに "ARM Translation Installer v1.1.zip" を ドラック&ドロップ
- エミュレーターに "Google Apps for Android 4.4.zip" を ドラック&ドロップ
- エミュレーターでGooglePlayを起動して確認
- 設定後、ドクターで検査(https://mynet-native.esa.io/posts/68#3-0-0)
- Android GenyMotionの場合 Appium Startより先に エミュレーターを起動しておく必要がある
起動と終了
起動
$ appium &
終了
$ killall -9 node
ドクター
$ appium-doctor $ appium-doctor --ios $ appium-doctor --android
Appium Settings
Android Settings
あくまでも目安
- App Path 設定
- Device Name ON "該当device名"
iOS Settings
あくまでも目安
- App Path 設定
- Force Device 設定
- UDID 不要
- Advanced > Use Native Instruments Library ON
Developer Stettings
あくまでも目安
- Enabled ON
- Use External NodeJS Binary ON "/usr/local/bin/node"
- Use External Appium Package OFF
- NodeJS Debug Port ON "5858"
- Break On Application Start OFF
各言語での利用方法
Objective-C(iOS / Android)
- テストコード作成
テスト実行
現状できたこと
[wd executeScript:@"UIALogger.logStart(\"Test Start\")"]; [wd executeScript:@"UIALogger.logPass(\"Test Success\")"];
- 後できるといいこと
- 画像検証 tuneupJS同様 imagemagickがあるといいかも?
- テスト結果をなにかしら出力
node.js(iOS/Android)
- テストコード作成
- node.js or mocha の利用
- APIドキュメント
- テスト実行
- node xxx.js
- mocha xxx.js
- 現状できたこと
- Waitかけれた
.sleep(10000) - Tapイベント送れた
- スクリーンショット
takeScreenshot()orsaveScreenshot("フルパス") - mocha なら テスト結果をコンソールに出力できる(後述有り)
- Waitかけれた
- 後できるといいこと
- tuneupJS との連携...難しそう
- 補足
mocha xxx.js --reporter xunit > xunit.xmlJUnit形式のレポート出力ができる
※ C#, Ruby, Java, Pythonについては未検証
テスト成果物
- Appium は基本成果物は出力しない
- iOSの場合は /tmp/appium-instruments に trace結果が出力される
- node.js は mocha を使用することで コンソールに出力可能
- Java は Sahagin を使用することでテストレポートの生成が可能(制約あり)
-> 自前での成果物生成するしか無い
mochaを利用した例

AppiumをObjective-Cで使う場合のアレコレ
- OSバージョン、デバイス名、ipa/apkパス を外からもらう
- Appium を Objective-C側から起動、終了させる
- Androidエミュレーター "Genymotion" を Objective-C側から起動、終了させる
- Appium の Session をちゃんと閉じる
- Genymotion、adb、UIAutomatorの機嫌について
- 概ねの原因
- 試行錯誤の解決方法
- エミュレーターの準備ができたかチェックする方法
- 独自エラーカウントでテスト中断機構
- ログ出力は printfより NSLogのほうがおすすめ
- テストレポートの出力
OSバージョン、デバイス名、ipa/apkパス を外からもらう
main の argc/argv から パラメーターを貰う
Xcodeの場合:

CommandLineToolの場合:
$ appiumTest_Android -app ~/MainActivity-release.apk -os 4.4 -device Nexus5 -AndroidTest
main から受け取った引数をなんやかんやする
SECapabilities *caps = [SECapabilities new]; [caps addCapabilityForKey:@"appium-version" andValue:@"1.0"]; [caps setPlatformName:@"Android"]; [caps setPlatformVersion:[TestUtil getTestOSVersion]]; [caps setDeviceName:[TestUtil getTestDevice]]; [caps setApp:[TestUtil getAppApkPath]];
Appium を Objective-C側から起動、終了させる
Appium.sh を用意して、NSTask で起動する
// Appium起動
NSTask* appium = [[NSTask alloc] init];
NSString* appiumPath = [NSString stringWithFormat:@"~/Appium.sh"];
[appium setLaunchPath:appiumPath];
[appium launch];
sleep(5);
~
// Appium終了
[appium terminate];
while ( appium.running ) //状態を確認してまだならsleep
{
sleep(1);
}
appium = nil;
Appium.sh
#!/bin/sh
#ユーザ環境のカスタマイズを読み込む
. ~/.bash_profile
#PATH="${PATH}":'/usr/local/bin':'/opt/local/bin'
#export PATH
#export JAVA_HOME='/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home'
#export ANDROID_HOME='/Users/[user]/android-sdks'
#export GENYMOTION_APP_HOME=/Applications/Genymotion.app
echo $PATH
#念のため殺しておく
adb kill-server
killall -9 node
#ログ無し、タイムアウト調整
#appium --log-level warn:error --device-ready-timeout 10000
appium
Androidエミュレーター "Genymotion" を Objective-C側から起動、終了させる
AndroidNexus5.sh を用意して、NSTaskで起動する 終了時は AndroidNexus5Stop.shを別途用意して NSTaskで実行する
// Androidエミュレーター起動
NSTask* task = [[NSTask alloc] init];
NSString* path = [NSString stringWithFormat:@"~/AndroidNexus5.sh"];
[task setLaunchPath:path];
[task launch];
sleep(40);
~
// Androidエミュレーター終了その1
[task terminate];
while ( task.running ) //状態を確認してまだならsleep
{
sleep(1);
}
task = nil;
// Androidエミュレーター終了その2
NSTask* genymotionKillTask = [[NSTask alloc] init];
NSString* stopPath = [NSString stringWithFormat:@"~/AndroidNexus5Stop.sh"];
[genymotionKillTask setLaunchPath:stopPath];
[genymotionKillTask launch];
while ( genymotionKillTask.running ) //状態を確認してまだならsleep
{
sleep(1);
}
genymotionKillTask = nil;
AndroidNexus5.sh
#!/bin/sh /Applications/Genymotion.app/Contents/MacOS/player --vm-name "Nexus5" --no-popup
AndroidNexus5Stop.sh
#!/bin/sh #パワーオフとADBも止める /Applications/Genymotion.app/Contents/MacOS/player --vm-name "Nexus5" --no-popup --poweroff --stopadb #VBoxManageも裏では起動しっぱなし、メモリの消費が高いので止める /usr/local/bin/VBoxManage controlvm Nexus5 savestate
Appium の Session をちゃんと閉じる
@autoreleasepool // autoleasepoolでスコープ・メモリ制御
{
SECapabilities *caps = [SECapabilities new];
[caps addCapabilityForKey:@"appium-version" andValue:@"1.0"];
[caps setPlatformName:@"Android"];
[caps setPlatformVersion:@"xx"];
[caps setDeviceName:@"xx"];
[caps setApp:@"xx"];
[caps addCapabilityForKey:@"deviceReadyTimeout" andValue:@"300"];
[caps addCapabilityForKey:@"androidDeviceReadyTimeout" andValue:@"300"];
[wd closeApp];
[wd quit]; // quit を呼んであげましょう
wd = nil; // nil代入はおまけ
}
Genymotion、adb、UIAutomatorの機嫌について
※ 実行中に強制終了をすると UIAutomator が機嫌を損ねる可能性があります。 次回起動時に UIAutomator関係でエラーになりやすくなります。
[31merror[39m: UiAutomator quit before it successfully launched
[36minfo[39m: [debug] Cleaning up appium session
[31merror[39m: Failed to start an Appium session, err was: Error: UiAutomator quit before it successfully launched
[36minfo[39m: [debug] Error: UiAutomator quit before it successfully launched
at [object Object].<anonymous> (/usr/local/lib/node_modules/appium/lib/devices/android/android.js:205:23)
at [object Object].<anonymous> (/usr/local/lib/node_modules/appium/lib/devices/android/android-hybrid.js:249:5)
機嫌が悪くなる概ねの原因
- Genymotion のエミュレーターが複数台、裏で生きていた
- adbに複数認識されているとUIAutomator error になる
- エミュレーター起動前にAppium側からAPKインストールなど処理が動いていた
- VBoxManagerによるメモリ圧迫(4台4プロセスx800MB消費)
試行錯誤の解決方法
- Genymotionエミュレーターの終了処理を実装する
- デバイス切替、起動時に adb の再起動&複数台生きている状態を無くす
- VBoxManagerの都度終了処理を実装
- NSTask終了時のrunningチェック でフロー確立
上記、解決方法で かなり改善した...が稀にまだ機嫌が悪い時がある PCスペック・メモリは高いほうがいい... エミュレーターの起動が遅くなるとSkip or wait, sleep せざるを得ない
エミュレーターの準備ができたかチェックする方法
SECapabilities *caps = [SECapabilities new];
~省略
SERemoteWebDriver *wd = [[SERemoteWebDriver alloc] initWithServerAddress~];
sleep(30);
NSLog(@">> currentActivity : %@", [wd currentActivity]);
NSLog(@">> isAppInstalled : %@", [wd isAppInstalled:@"jp.co.mynet.eof"] ? @"YES" : @"NO");
// この時点でエミュレーターが起動し、アプリインストール完了していないとNG
while ( ![wd isAppInstalled:@"jp.co.mynet.eof"] )
{
sleep(1); //起動準備ができていない while or Skipさせる
}
独自エラーカウントでテスト中断機構
エミュレーターの起動エラーなどが現状では起こりうる あまりにも長時間のテストの場合、時間が勿体無いので途中でテストを停止させる 例えばエラー5回で テストを中断し、正しい終了を行い、Assertを投げるなどの機構を作る
ログ出力は printfより NSLogのほうがおすすめ
Jenkins で実行、出力を行う場合 printf は、スタック後回しにされまとめて出力される NSLog は、即出力され時系列を維持できる
テストレポートの出力
自前で頑張る!! ほかいい方法があれば随時募集!!
- Junitのフォーマットでxmlを吐き出し、Jenkinsに読み込ませる
- ImageMagickの比較は htmlを生成し生成物から参照させる