七転び八起きなAppiumを使ったモバイルテストのたしなみ

  • 35
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

おはようございます。気づけばクリスマス、2014年として過ごす毎日もあと1ヶ月を切る日々になってきました。Selenium/Appium Advent Calendar 2014の2番目、12月2日の記事です。

テストエンジニアしています@Kazu_cocoaです。これがQiitaへの初投稿になります。

さて、何を書こうか、と思っていたのですが、Appiumを使いモバイルアプリのテストを書いている上で遭遇した ツラみ Tipsをつらつらと書いていこうと思います。ここでいうテストは、モバイルアプリを対象としたE2E、システムテストレベルの話しです。

ちなみに、私は主にAppium x Turnipの組み合わせでテストを書いています。

はじめに

Appiumとは、みたいな基本的な話しは日本語の資料も(古いものも多いですが)出回り始めていると思いますので割愛します。いくつかのWebサイトをご覧ください。また、Appiumはここ1年くらいでv1.0がリリースされたり、1ヶ月~2ヶ月単位で1.1, 1.2, ...と更新を繰り返し、安定性も高まってきました。さらにはv2.xのアーキテクチャの話しがでるまでに勢いづいているので、英語に敷居を感じない方は本家を覗くと良いです。ドキュメントも、コマンドの詳細なんかは古いものが散見されますが、大枠の理解には差し支えないと思います。
WebPage: http://appium.io/
GitHub: https://github.com/appium

環境

本記事に書く内容は、Appium 1.3.3 までの話しです。

実行環境としては以下を扱っています。

  • Mac OS X Maverics / Yosemite
  • Xcode 4.6.3, Xcode 5.1.1, Xcode 6.0, Xcode 6.1
  • Appium@1.3.3以下
  • ruby_lib@4.1.0
  • RSpec 3 x Turnip
  • Android SDK 23

基本、Appiumを中心にiOS/Androidの話題を書いていますが、Appium自体が原因ではなく、その実行環境(XcodeやInstrumentsなど)含めて遭遇した問題などに対して書いています。

蛇足なのですが、このAppium1.3.3は1.3.2としてリリースされる予定だったのですが、npmにとして管理されているバージョニングの都合で1.3.3としてリリースされました。

内容: iOS編

1. Xcode6.0以上でビルドした成果物をiOS6.1シミュレータでテストしたい

最近では対応端末も減ってきたとは思いますが、歴史あるアプリケーションを開発している方やiOS8が発表されるより前の段階なんかでは、iOS6対応を行っている方々も多いと思います。一方、Xcode6のDevicesを見ると、iOS6.1のシミュレータが消えているという現象に出会っている方もいることでしょう。

xcodebuildでビルドしたシミュレータで実行可能な.appアプリを作成した後、Xcode5.1.1 or Xcode4.6.3に xcode-select -s でXcodeを切り替えた後テストを実施してください。Xcode5.1.1とXcode4.6.3はiOS6.1のシミュレータを持つので、Xcode6系でビルドしたバイナリを、iOS6シミュレータで起動することができます。最近では任意のシミュレータに直接インストールする方法が散見されるようになりましたが、instruments経由でインストールする方法はXcode6以前よりも存在していました。

ただし、落とし穴が。Xcode5.1.1のiOS6.1シミュレータは、 時折、アプリのインストールに失敗する既知の問題 があります。感覚的に、2分の1くらいでしょうか。なので、ちゃんとiOS6.1で回したいなら、Xcode4.6.3をお使いください。ただし、昨今ではシェアもシェアなので、iOS6は考えないか、テストするにしても実機で確認、でカバーすることが現実解だと感じています。

2. アプリがログを出力しすぎてiOS8 Simulatorが動かない

Debugビルドなんかでは、NSLogなんかでログを出力しつつ動作確認を行う方は多いと思います。iOS8.0シミュレータでは、instruments経由でアプリを起動させたとき、アプリは起動するのですが、起動して数秒のうちにアプリがフリーズしたかのように反応しなくなることがあります。

原因もわからず色々調べていたのですが、アプリケーションログが大量に出力されているとiOS8 Simulatorがハングアウトして反応しなくなることがあるそうです。ものは試しにログ出力を抑えた版でアプリを起動してみたら、正常に動作した...

3. シミュレータのファイル構造が違う...

iOS8 Simulatorを境いに、シミュレータのアプリデータの保存先などのファイル構造が変化しました。そのため、AppiumがiOS8対応を進めている間はテスト対象のアプリのplistが削除されず、過去の設定値が残り続ける状態が続く現象が起きていました。Appium本家にPR出したりして今は正常ですが、設定ファイルの場所が変わっていることとか、構成ファイルとにらめっこしながらでないと気付けないので結構辛い。

4. 突然消えたiOS8.0 Simulator

Xcode6.0 => Xcode6.1へアップグレードしたとき、iOS開発者なら誰もが感じたと思います。Devicesを見ればiOS8.0のシミュレータがなかったことにされていた...
不具合の多かったiOS8.0なので、やはり市場に出す前に動作確認をしておきたい。一方、Xcode6.0でなければ使うことができないので、それらをカバーするにはsudo権限の必要な xcode-select -s の操作をシナリオの合間に挟まなければいけません。それは少し辛い。。。

5. 突然消えたSafari.app

Appium1.2系の頃だったかと思いますが、Appiumをfull reset指定してSafariでテストを実行すると、気づいたらSafari.appが削除されていたことが... 修正済みらしいですが、当時は少し焦りました。

6. 日本語ローカライズファイルを指定したのに英語ローカライズでアプリが起動する

Appiumは、capabilityに localizableStringsDir 要素を指定したり、Appium server起動時に同様の要素を指定すると、特定のローカライズファイルを使いアプリを起動させることができます。iOS7.1 Simulatorまではそうでした。一方で、iOS8 Simulatorではそれが叶わず...

Localization and Keyboard settings (including 3rd party keyboards) are not correctly honored by Safari, Maps, and developer apps in the iOS 8.1 Simulator. [NSLocale currentLocale] returns en_US and only the English and Emoji keyboards are available. (18418630, 18512161)

https://developer.apple.com/library/ios/releasenotes/DeveloperTools/RN-Xcode/Chapters/Introduction.html#//apple_ref/doc/uid/TP40001051-CH1-SW1.

対策として、例えば キャンセル/Cancel のように英語/日本語と環境により変化するアラート/アクションシートに対する操作をすべて ios8? のようなメソッドを定義して分岐して回りました。かっこよくないですが、致し方ない... Xcode6.2の頃には治ってるといいな...

7. Screenshotによる画像の保存に失敗する

Appiumが端末のスクリーンショットを撮影するとき、デフォルトだと /tmp/appium-instruments/ 配下に一時的に画像を保存し、その後、特定のパスにリネームして画像を移動させます。通常は問題無いのですが、/tmp/appium-instruments/ 以下には画像が存在するのに、スクリーンショットを移動させることが正常にできずにエラーになる、もしくは画像データが破損することがあります。

GitHubにもissueを作成していたことはあるのですが、原因はまだ特定されていなかったような気がします。なのですが、1.3.4で screenshotWaitTimeout のオプションが追加されるようです。最近、試しにそこで20秒にタイムアウトを設定すると問題が再現しなくなりました。Appiumではなく、OS側のIO関連の可能性もありそうです。

なお、Screenshotは失敗するがシナリオ自体は正常に進むケースがあったため、スクリーンショット失敗によってテスト自体が異常終了しないように対応しています。

8. UIASecureTextFieldに文字列を入力できないことがある

iOS8.0や8.1系にて、UIASecureTextFieldがみつからない(要素の取得ができない)場合があります。その場合、以下のように要素取得前にタップすると、UIASecureTextFieldを取得できました。

require 'appium_lib'

account = 'アカウント'
pass = 'パスワード'

# 通常
first_ele('UIATextField').send_keys account
first_ele('UIASecureTextField').send_keys pass


# iOS8.xなんかの、UIASecureTextFieldをたまに取得できない場合
first_ele('UIATextField').send_keys account

tap_xpath("#{get_root_xpath}UIATableView[1]/UIATableCell[2]")
xpath("#{get_root_xpath}UIATableView[1]/UIATableCell[2]/UIASecureTextField[1]").send_keys pass

9. XPath使う時はSleepも...

XPathによる要素の直接指定は、accessibility_id strategyよりも汎用性が高いです。一方で、XPathのパスで要素を指定する場合、現在表示されている画面にパスが存在すればその要素に対してなんらかの操作を実施します。これは、意図しない画面においても、パスが一致すればそれが実行されることを意味します。

要素の取得にはimplicit waitで要素が取得できるまで一定時間待つことも有効ですが、このようにXPathを指定するときはSleepを少しかませた方が良さそうな時も多々あります。個人的には、accessibility_idのように、アプリや画面において確実に特定可能な条件を指定できる状況以外では、0.5~1秒程度Sleepさせています。

10. iPhone6/6+対応/未対応アプリとでinstrumentsが取得できるXPathが違う

一時はMavericksとYosemiteという実行環境の違いからXPathが異なるものと思っていたり、iPhone6/iPhone6Plusとそれ未満とでXPathに違いがあると考えていました。一方で、実は iPhone6/6+未対応アプリをiPhone6/6+で動かす 場合と iPhone6/6+対応アプリをiPhone6/6+やiPhone5s以前とで動かす 場合とで、instruments経緯でアプリから取得可能なXPathが異なることが分かりました。実機でも同じようなもの?のようです。非対応アプリを描画するときに、1つUIAWindowを間に多く挟んで、表示をエミュレートしているのかもしれません。

例えば、以下のようなある任意のUIATableCellを選択しようとしたとき、

  • iPhone5s、iPhone 6 / 6 Plus(対応アプリ)
    • //UIAApplication[1]/UIAWindow[1]/UIATableView[1]/UIATableCell[1]
  • iPhone 6 / 6 Plus(未対応アプリ)
    • //UIAApplication[1]/UIAWindow[2]/UIATableView[1]/UIATableCell[1]

のXPathでそれぞれ選択可能でした。私の経験した限りでは、異なるのは UIAWindow[1] の箇所のようなので、そこまでのXPathを端末によって返し分けるメソッド用意しておけばメンテナンスが容易になりそうです。以下のような形で。

def base_root_xpath
  return '//UIAApplication[1]/UIAWindow[2]/' if 条件
  '//UIAApplication[1]/UIAWindow[1]/'
end

11. accessibilityIdentifierを使う

UIAutomationを使うためにAppleが提供するaccessibilityIdentifierを使うと、UIAutomationのためだけの要素をアプリに与えることができます。結構前から提供されていますね。accessibilityLabelはアクセシビリティの音声読み上げ有効時に読まれるので、テスト目的であればLabelは良くなさそうです。これを

self.accessibilityIdentifier = @"sample"

のように、NSLocalizeを与えることなく指定すると、UI越しに見たまんまの操作からは少し距離が置かれますが、変更には強いシナリオの作成を支援してくれます。この場合は、accessibility_idのStrategyを使って値を取得するようにしましょう。

12. waitでタイムアウトした時のエラーを見やすくする

ruby_libを使っていると、以下のようにnekoメソッドが成功するまで一定時間待つwaitメソッドが提供されています。

wait { neko }
wait_true { neko }

このwaitには

wait(message: "エラーになった時のメッセージ") { neko }

のように、エラーに表示される任意のメッセージを与えることができます。どの要素が見つからなかったのかなど、テスト失敗時のヒントをあたえてくれると思います。

13. UIAutomationのスクリプトを直接実行する

例えば、以下のようなスクリプトを実行することで、アラートに表示されているbutton要素をタップする、という処理を実施することができます。

Appiumというか、UIAutomationは存在しない要素を送るとエラーを返してくるので、アラートのように表示される/されない場合がある時なんかは、要素の有無の条件式と併用すると良さそうです。

require 'appium_lib'

text = '何かのボタンのラベル名'

execute_script("$.mainApp().alert().buttons()['#{text}'].tap();")

14. xxまでスクロールする

現在提供されている以下のxxまでスクロールするというスクリプトは削除されることが決まっています。

execute_script('mobile: scrollTo', element)

一方で、xxx(要素、 or 座標)までスクロールしたいという要望もあるかと思います。現在、Androidではuiautomatorを使ったスクロール例が提供されていますが、iOSでは特に記載されていません。

iOSではその代替として、UIAutomationの scrollToVisible や、 moveTopress / release を組み合わせた方法で同様な事を実現できます。

execute_script("au.getElementByAccessibilityId('#{element}').scrollToVisible();")

ただ、この後に例えばclickのコマンドを送ってもinstrumentsからエラーが帰ってくるので、シミュレータ自体が少し不安定な印象を受けています。必要でないなら必要以上に頑張らないことが懸命かもしれません。

内容: Android編

15. XPathの指定が実機/エミュレータで違う...

まだ軽くしか書いていないのですが、AndroidではXPathによる要素指定において、実機/エミュレータで異なることが多々あります。OS 4.4以降は、Google API提供のx86エミュレータも提供され始めたので、比較的ストレスなく多くのアプリはエミュレータで動きそう。テスト環境をエミュレータに絞って動作確認するというのも良さそうですね。

内容: 番外、RSpec/Turnipとの組み合わせ

Appiumに限った話しではないのですが、知見の端っこを小ネタとして残そうと思います。

16. シナリオ失敗時にHookする

シナリオ失敗時の情報は、不具合解析に価値のあるものです。以下のように、spec_helper.rbにafter hookを指定しておくと、何かエラーが発生した時にその操作を実施してくれます。ここでスクリーンショットなどを指定しておけば、容易に失敗時の情報を集めることができるようになります。

RSpec.configure do |c|
  c.after(:example, type: :feature) do |example|
    if example.exception
      # スクリーンショットの保存とか
    end
    $driver.driver_quit if $driver
  end
end

ここら辺を見ると、自分たちでテスト失敗時の情報を取得するような機能も既存のテストフレームワークを利用すると容易に実現できますね。始めのうちは、機能が肥大なテストシステムはいらない、ということ。

内容: 環境

17. Real device vs Simulator

Real Deviceで確認できることが理想的ではあるのですが、すべてReal Deviceで実施するのも大変です。Appiumを使って実施するテストの多くは、たいていは機能的な不具合の検出や、描画崩れの検出かと思います。なので、実機とシミュレータとで差分が発生する箇所をある程度知って(例えばカメラとか、システム側設定だったり、描画関連で)おけば、そこを除くと多くはシミュレータによる代替でも十分だと思います。

一方、例えばユーザビリティであったり、端末資源を考慮した性能試験など、全く異なる非機能に言及するようなテストを実施する場合は話が異なります。Appiumではそれらを計測するために使うことができるテストケースは非常に限られるでしょう。その場合はちゃんと実機で行うほうが良いと思います。

こういう品質に対する分析やテスト内容として何をやるか、というあたりは、お近くのテストエンジニアに話を聞くと良いと思います。きっと、テスト計画や戦略など、有益な知見を持っているはずです。

加えておくと、Androidでは実機インストールが楽なのと、エミュレータよりも実機のほうが高速にアプリが動作します。Androidでは実機のほうが何かと楽ですよね。

18. なぜAppium?

CalabashなどのSDKを埋め込んだテストも散見されます。一方で、それはSDKを埋め込んだ専用ビルドが必要になります。過去、私は実際にDebug/Test目的で埋め込んだライブラリが原因でUIが崩れ、それらを除くと問題なくUIが表示された、という事例に出会ったことがあります。Appiumで確認するレベルのテストの多くは、最終的なE2EやUIの崩れ確認だと思いますので、Appiumが個人的には好きです。

19. AppiumにRecord/Playは必要ではない?

Appiumは標準ではRecord/Playの機能は提供していません。アニメーションの正しさも確認したいのであれば、録画(Record)は重要な機能ですが、アニメーションの多くはシステムによっている部分があります。また、アニメーションの多くは機械的に正しい/正しくないの判断は難しいです。逆に、不具合解析のための機能として必要であれば、After Hookなどを用いて同等のことは比較的すぐ可能なので、Record/Playを結合してAppiumが肥大化、不安定になるよりは今の状態で十分だと思っています。必要であれば、ほかライブラリと組み合わせて同様の機能を用意することのほうが現実的そうです。

20. 費用的なもとは取れそう?

私が実際にAppiumを利用したところでは、アプリのUIリニューアル、iPhone 6/6 Plus の解像度対応のようにUIへの影響が大きく発生する変更を半年のうちに2回実施することになりました。そのときは、人手だと1回すべて回すのに数時間はかかるテストスイートを毎晩のように実施し、短い期間で不足箇所を検出していました。1つの組み合わせ(iPhone 6 Plus x iOS8.1など)に対してテストケースでは100弱、スクリーンショットの枚数では200枚超えるくらいでした。今でも回していますが、iOS8やiPhone6の組み合わせも含めて、6時間程度あれば4、5パターンくらい試せています。ちなみに、人手だと2種類の組み合わせを実施するのに1人日くらいの量です。

逆に、そのような高頻度な修正を行わないような企業や製品だと、あまり実りのある成果には繋がらなさそうです。Seleniumと同様ですん

Appiumのシナリオに対するメンテナンスコストの話しがありますが、プログラミングを全く書けないという人でなければ元は取れそう、という感覚です。参考に、iOS8やXcode6に対して純粋にこのAppiumで対応したのは本家へのコミット含めて3~4人日くらいです。現在はAppium自体も安定しているので、英語ドキュメンドを読めないわけでなければ、おそらく学習コスト含めて小さくいけるのではないかと思います。(メソッドの共通化などは基礎的な教養としてありますが...)

21. モバイルアプリのテストってどこまで進んでいるの?

昨今のWebアプリはテストを高速に実施可能なフレームワークが成長/成熟しているように思えます。一方、モバイルアプリはUIに影響されるところが多く、LibraryやModelに対するユニットテストはかけても、その上のControllerやViewに関しては描画との疎結合や独立しての実装が難しく、現実的なものは難しいです。なので、今の時代ではAppiumのように描画を含んだテスト自動化が重要なツールとして存在しています。(これからこういう方面も成長させていきたいですね)

昨今の段階ではテスト自動化のピラミッドのような綺麗な三角形にすることは難しい分野ですが、OS側の成熟も進んでいくにつれてテスト自動化も実施しやすくなってはいるので、これからの流れにも注目ですね。

最後に

Appiumに関わることから、それに限らずつらつらと書いてきましたが、国内ではAppiumを実際に運用してどうだとかいう話しの知見は少ないようです。そこらへんの知見を共有していくことで、みなさんがAppiumを触る頃に同様の問題にぶつかったときに、それを解決するための足がかりになるような情報を共有していきたいですね。(私も情報ほしい)

さて、残す数週間も頑張りましょう!

この投稿は Selenium/Appium Advent Calendar 20142日目の記事です。