AppiumによるUIテスト自動化

本記事はマイネット Advent Calendar15日目の記事です。

今回はネイティブ周りを担当している山木がAppiumによるUIテスト自動化についてお送りします。

Appiumの特徴

  • WebView、フルネイティブ、Unity、cocos2d-x 可能
  • iOSAndroid 両方利用可能(ただしテストコードの共有はできない)
  • 実機、シミュレーターでの実行 可能(Androidエミュレーター Genymotionも可)
  • コマンドからの実行可能
  • スクリーンショット可能(一部言語制限)
  • テストコードは C#RubyObjective-CJava、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の場合に必要

bash_profile(例)

export JAVA_HOME='/usr/libexec/java_home'
export ANDROID_HOME='/Users/[user]/Library/Android/sdk'

起動と終了

起動

$ 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-CiOS / Android

  • テストコード作成
    • Xcode Command Line Tool で 吐き出したコードを組み込み、実行ファイルを作る
    • Selenium.framework は Appium.app の中から抜き取った( Git
    • CommandLineToolの場合frameworkは組み込めないので "/Library/Frameworks"に格納とXcodeにバインド
    • [iOS / Android] テストコードは別々の必要がある(Elements階層が異なるため)
  • テスト実行

    • ターミナルで $ appium & 実行
    • ビルドしたCommandLineToolを 実行
    • [Android] エミュレーターを事前に起動する必要がある
  • 現状できたこと

    • [iOS] Waitかけれた [wd setImplicitWaitTimeout:10000]; //10sec
    • [Android] Waitかけれた sleep(10); //10sec
    • [iOS/Android] Tapイベント送れた
    • [iOS/Android] スクリーンショット取れた [wd screenshot] あとは自前で保存
    • [iOS] UIALogger.logStart()、UIALogger.logPass() など利用できた
[wd executeScript:@"UIALogger.logStart(\"Test Start\")"];
[wd executeScript:@"UIALogger.logPass(\"Test Success\")"];
  • 後できるといいこと
    • 画像検証 tuneupJS同様 imagemagickがあるといいかも?
    • テスト結果をなにかしら出力

node.js(iOS/Android

  • テストコード作成
  • テスト実行
    • node xxx.js
    • mocha xxx.js
  • 現状できたこと
    • Waitかけれた .sleep(10000)
    • Tapイベント送れた
    • スクリーンショット takeScreenshot() or saveScreenshot("フルパス")
    • mocha なら テスト結果をコンソールに出力できる(後述有り)
  • 後できるといいこと
    • 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を利用した例

スクリーンショット 2015-11-05 18.24.09.png (112.1 kB)

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の場合:

スクリーンショット 2015-11-19 11.57.22.png (52.7 kB)

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を生成し生成物から参照させる

参考