Intelligent Technology's Technical Blog

株式会社インテリジェントテクノロジーの技術情報ブログです。

Appiumでモバイルアプリのテストを自動化【iOS Mobile Safari編】

【追記2014/7/7】
記事中のAppiumのドキュメントへのリンク先は削除されてしまったようです。その代わりに公式サイトのドキュメントが整備されています。

【追記2014/7/16】
Appiumの新しいバージョン(1.2.0)に関する記事を書きました。

こんにちは、間藤です。

今回がiOS編の最後です。これまでは「アプリのテスト」だったのですが、今回のテスト対象はWebアプリケーションです。これをテストするのに、Mobile Safariを利用します。
ただ単に手動でWebアプリケーションをテストするだけなら、テスト用のiPhone端末なりiPad端末なりを用意すればよいわけですが、今回紹介する自動テストを実現しようとした場合、Macが必要だったり、iOS Developer Programへの登録が必要だったりと、手動テストでは不要な環境、手続きが必要になります。ネイティブアプリやハイブリットアプリであれば、MaciOS Developer Programへの登録はそもそも必要*1ですが、Webアプリケーションのテストのためにこういった環境を用意するのかという点は考えどころだと思います。

ちゃんとは動いてません

最初に書いておきますが、Mobile Safariを利用した自動テストは、正常に動作させることができていません。詳細は後で書きますが、以下のような問題が起きています。

  • Appiumのプログラムを1箇所(実機の場合は2箇所)修正しないと、テストがエラー終了してしまいます
  • テスト終了時にアプリが終了しません(シミュレータも起動したまま)
  • キャプチャ画像を取得することができません(解決策があり)

実行環境等

Safariを起動するために、SafariLauncherというアプリを利用します。このアプリがテスト対象というわけではありませんが、このアプリを利用するという意味で一覧に記載してあります。テストスクリプトは提供されているものを使いますが、何点か修正が必要です。

ホストPC OS X Mavericks
Xcode 5.0.2
Appium 0.12.0
iOSシミュレータ 7.0.3
iOS実機 7.0.4
テスト対象アプリ SafariLauncher、Mobile Safari
テストスクリプト sample-code/examples/java/junit/src/test/java/com/saucelabs/appium/SafariTest.java

事前準備

テスト対象アプリを"safari"と指定すると、AppiumはSafariLauncherというアプリをインストールしようとします。

capabilities.setCapability("app", "safari");

このアプリのzipアーカイブがAppiumサーバを起動したディレクトリ配下(./build/SafariLauncher)に配置されていなければならないようです。以下、Appiumが出力したログの抜粋です。(実機の場合、SafariLauncher.zipとなります)

info: Using local zip or ipa from desiredCaps: ./build/SafariLauncher/SafariLauncherSim.zip

このパスにzipアーカイブが配置されていないと、テストはエラー終了してしまいます。cloneしたディレクトリ配下でreset.shを実行することで、このzipアーカイブが作成されますので、以下のコマンドを実行してください。

> cd appium
> ./reset.sh --ios --real-safari

実機用のzipアーカイブを作成するためには、--real-safariオプションをつけて実行する必要があるようです。詳細は、以下のドキュメントを確認してください。

提供されているテストスクリプトもいくつか修正します。
まず、iOSバージョンを指定している箇所を、実際に起動されるシミュレータのバージョンに合わせるよう修正します。

//capabilities.setCapability("version", "6.1");
capabilities.setCapability("version", "7.0.3");

これが揃っていないと、テストが正常に動作しなくなってしまいます。
また、サンプルのテストシナリオを1行削除します。

//assertTrue(driver.findElement(By.id("your_comments")).getText().contains("This is an awesome comment"));

このシナリオでは、submit後に検証を行っているのですが、submit後のページロードを待機できていないため、このアサーションは大抵の場合失敗します。ひとまずテストを動作させるだけなら、このアサーションは不要なので今回は削除します。

事前準備その2(Appiumのバグ?)

Appium本体のプログラムを2箇所修正します。

テストスクリプトとAppium間でやりとりする際、Appium側でセッションIDを発行しています。詳細は、JsonWireProtocolの仕様を確認してください。AppiumのプログラムでセッションIDを発行している箇所を探すと、appium.jsに見つけることができます。

【/usr/local/lib/node_modules/appium/lib/appium.js】

this.sessionId = UUID.create().hex;

通常はこのセッションIDがそのまま利用されるのですが、Safariを利用する場合は、これが後で上書きされるようになっていました。

【/usr/local/lib/node_modules/appium/lib/appium.js】

var onStart = function(err, sessionIdOverride) {
  if (sessionIdOverride) {
    this.sessionId = sessionIdOverride;
    logger.info("Overriding session id with " + sessionIdOverride);
  }

sessionIdOverrideに何が渡されるのか、プログラムを追いかけてみたところ、呼び出し元は以下のようなコードでした。

 this.curWindowHandle = pageIdKey.toString();
 cb(null, {
   status: status.codes.Success.code
   , value: ''
 });

つまり、文字列ではなく、オブジェクトリテラルが渡されます。ログには、以下のように出力されています。

info: Overriding session id with [object Object]
info: Device launched! Ready for commands (will time out in 60secs)
info: Appium session started with sessionId [object Object]

そして、このようなセッションIDが返されると、テストスクリプトでエラー終了してしまうようなのです。
どうしてこのようなプログラムになっているのか解析するのは諦めて、ひとまず上書きしている箇所をコメントしてみたところ、エラー終了しなくなりました。

//    this.sessionId = sessionIdOverride;

GitHubにも似たような問題がレポートされていました。

実機で実行する場合、もう1つ問題が起きます。SafariLauncherのインストールです。Appium内部で、インストールにfruitstrapを利用しています。そして、fruitstrapコマンド実行が異常終了するため、テストも失敗してしまいます。そこで、SafariLauncherの実機インストールは、Xcodeで行うことにして、fruitstrapコマンドが実行されないようにプログラムを書き換えました。

【/usr/local/lib/node_modules/appium/lib/devices/ios/ios.js】

IOS.prototype.installSafariLauncher = function(cb) {
  cb();

  //this.isAppInstalled("com.bytearc.SafariLauncher", function(err) {
  //  if (err) {
  //    this.installApp(this.app, cb);
  //  } else {
  //    cb();
  //  }
  //}.bind(this));
};

元のコードをコメントアウトして、コールバック関数をすぐに呼び出すようにしています。

シミュレータで実行

まず、Appiumサーバを起動します。前述のとおり、SafariLauncherのzipアーカイブが./build/SafariLauncher以下に配置されていなければなりませんので、起動時のカレントディレクトリに注意してください。

> cd appium
> appium

そして、テストを実行します。

> cd sample-code/examples/java/junit
> mvn test -Dtest=jp.co.iti.appium.SafariTest

テストが終了しても、シミュレータは起動したままとなります。Safariがフォアグランドになっていて、Appleのホームページが表示されます。

実機で実行

実機で実行する場合は、ios-webkit-debugger-proxyを起動しておきます。

> ios_webkit_debug_proxy -c (実機のUDID):27753

次に、Appiumサーバを起動します。(実行する際のディレクトリに注意)

> cd appium
> appium -U (実機のUDID)

そして、テストを実行します。

> mvn test -Dtest=jp.co.iti.appium.SafariTest

テストが終了しても、Safariがフォアグランドになっています。ネイティブアプリやハイブリットアプリでは、起動したテスト対象アプリは、テスト終了と同時に終了しますが、Safariは間接的に起動しているだけなので、これは仕方のないことなのかもしれません。

キャプチャできません(解決策あり)

ネイティブアプリ編、ハイブリットアプリ編では、キャプチャ画像の取得は問題なく実現できました。しかし、Safariのテストでは、キャプチャが失敗します。ログを解析すると、気になる箇所を見つけました。

info: [INST] 2013-12-19 03:14:10 +0000 Fail: Could not start script, target application is not frontmost.
Instruments Trace Complete (Duration : 34.259861s; Output : /Users/matoh/Appium/appium/instrumentscli0.trace)

info: [INSTSERVER] Instruments exited with code 0

"target application is not frontmost"というメッセージの後、Instrumentsが終了しています。Instrumentsには、操作対象としてSafariLauncherを指定していますから、Safariがフォアグランドになっているのは想定外だ、ということなのだと思います。そして、キャプチャを取得する際は、AppiumはInstrumentsにリクエストするので、キャプチャ取得できなくなっていると考えられます。

Instrumentsを終了させないための対策、あるにはありました。
SafariLauncherは、openURLでSafariを起動しているのですが、しばらく待って(カウントダウンして)からSafariを起動しています。「なぜこのようなことをやっているのだろう?」と思っていたのですが、実はこの「待ち時間」がInstrumentsを終了させないために重要だということに気付きました。待ち時間は、SafariLauncherの設定画面で変更できます。(以下のスライダーで変更)

f:id:IntelligentTechnology:20131219133543p:plain

つまり、Instrumentsが起動されて対象アプリを確認するときにはSafariLauncherがまだフォアグランドにいる状態にして、その後Safariに移行することで、Instrumentsが終了しないようにする作戦です。
私の環境では、デフォルトの設定だと待機時間が短すぎて、Instrumentsが対象アプリを確認する瞬間には、Safariに切り替わってしまって、上記のログのようになってしまっていました。そこで、待ち時間を長めに調整して何度かトライしてみたところ、Instrumentsを終了させずにテスト継続することができました。

それにしても、無理やりな感は否めません。

トラブルシューティング

テストが失敗したときなどにInstrumentsのプロセスが残ってしまうことがあると書きましたが、シミュレータのプロセスが残ることもあるようです。
この状態でテスト実行すると、以下のようなログが出力されて、テストは失敗します。

info: [INST STDERR] 2013-12-18 15:16:48.574 sim[12673:303] Multiple instances of launchd_sim detected, using com.apple.iphonesimulator.launchd.4840458d instead of com.apple.iphonesimulator.launchd.2c713540.

この場合、問題のプロセスを見つけてkillしてください。

> ps -ef | grep launchd_sim

まとめ

以上でAppiumに関する投稿は一区切りとします。余力があれば、Android編を続けたいと思いますが、他のトピックに興味が向いてしまう可能性もあるので、ひとまずはこれで終わりということにしておきます。

今回の「Appiumお試し」は、簡単ではありませんでした。何かしら問題が起きて原因を調査するという繰り返しでした。現在も活発にアップデートが行われているようですから、将来はもう少し簡単に使えるようになるかもしれません。また、今回は実用面の評価はほとんどできませんでした。ちゃんとしたテストを実施しようとすると、「あれができない」「これができない」といった問題に直面することになるでしょう。
また、そうやって苦労して作成したテストシナリオが将来にわたっても利用可能かも気になるところです。モバイルデバイスのOSは、アップデートが激しいですから、自動テストツールがその変化に追いついていけるのか、キャッチアップのためのコストがかかりすぎないか、といったことも気にしなければならないでしょう。

自動テストが動くさまは、見ていてなかなか気持ちが良いのですが、実際に導入するにはそれなりの覚悟が必要だなと感じました。

*1:Monacaのような開発環境を利用していれば話は別です