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.xml
JUnit形式のレポート出力ができる
※ 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を生成し生成物から参照させる