読者です 読者をやめる 読者になる 読者になる

iOSアプリデザインリニューアルの舞台裏の舞台裏

技術部の松尾(@Kazu_cocoa)です。

iOSアプリデザインリニューアルの舞台裏でも書かれていた、" 修正期間中は毎日夜間にアプリケーションの全画面のスクリーンショットを記録するスクリプトを実行し、画面崩れが起きてないか、新デザイン未反映の画面はないか、進捗状況の確認に利用していました。"の舞台裏を少し書いてみようと思います。

はじめに

モバイルアプリケーションのテスト環境はまだまだ成長中で、様々なツールが飛び交っていることかと思います。ここでは、E2Eテストに対しての話題に絞り、使っているツール、シナリオの書き方、クックパッドでは、という話しをします。この記事におけるE2Eテストは、UIからの操作によりユーザの操作を模倣して実施するテスト、という意味合いです。

ツール

E2Eテストを自動化する為のツールの選定には以下を気にしていました。

  • OSの更新に追従できそうなもの
  • 特別なテスト用ビルドが必須ではない
  • UI越しの操作をシナリオとして記述、実施できる
  • (できれば)Ruby, Groovy, Pythonなどで記述したい

それらの点から、いくつかツールを検討し、結果、Appiumを選択しました。私がAppiumの状況を追い始めてもうそろそろ1年が経過しようとしている時期ですが、バージョンも1.0を超え、現在も活発に開発が続いているOSSです。

このブログを書いているときは、Appiumは1.2.2が最新です。

Appiumとは

Appiumに関する踏み込んだ話しは本家を参考にしてください。以下では、今回説明に使うAppiumのiOS向けな技術的概要を記載します。

全体のアーキテクチャは以下の図の通りになります。

http://www.3pillarglobal.com/insights/appium-a-cross-browser-mobile-automation-tool より

要素ツール

  • WebDriver
  • instrumentsUI Automation
    • Appleが提供する解析ツールなどのツール群の総称です。
    • UI AutomationはInstrumentsの一部で、特に外部からJavaSctiptで記述されたスクリプトによりUIの操作を可能とする機構です。
    • Appiumは、UIに対する操作はiOSが提供するUI Automationの機構を利用しています。

Appiumのシナリオを記述するために、既にいくつか公式にライブラリが提供されています。RubyやPython、Javaなんかのライブラリも提供されています。

Appiumを使ってアプリを起動する

簡単に、実際にAppiumを使いiOSアプリをシミュレータを用いてインストール・起動する所までの流れを記載します。Appiumを使うまでに、実際にInstrumentsを使ってみることもしてみます。

テストする成果物について

iOSのアプリは、.appのアプリとして生成されたものをアーカイブして.ipaファイルを作ります。iOSシミュレータでは、この.appのファイルをインストールし、起動、操作することになります。xcodebuildコマンドをインストールしていると、以下の通りのコマンドを使うことで、OUTPUT_PATHで指定したパスに.appのアプリを出力することができます。xcodebuildに関しては、こちらのURLを参照ください。

$ xcodebuild \
  -workspace "WORKSPACE" \
  -scheme "SCHEME" \
  -sdk "iphonesimulator8.0" \
  -destination "platform=iOS Simulator,name=iPhone 5s,OS=8.0" \
  -configuration "CONFIGURATION" \
  SYMROOT="$OUTPUT_PATH" \
  DSTROOT="$OUTPUT_PATH" \
  clean build

ここで、OUTPUT_PATH以下に出力された.appファイルを cookpad.app として、以下の話しを進めていきます。

Instrumentsを使う

Appiumでアプリを起動するまえに、基本としてinstrumentsを使いシミュレータへcookpad.appをインストールするところを模倣してみます。Xcodeのコマンドラインツールがインストールされていれば、通常はinstrumentsコマンドを利用可能です。以下のようにコマンドを直接入力することで、シミュレータを起動してcookpad.appをインストールすることが可能です。

$ instruments -t "/Applications/Xcode.app/Contents/Applications/Instruments.app/Contents/PlugIns/AutomationInstrument.bundle/Contents/Resources/Automation.tracetemplate" cookpad.app

OSにバンドルされたinstumentsを使うと上記のように操作可能なのですが、実際はAppiumはカスタムしたinstrumentsを経由してアプリのインストール、UI Automationへ命令を出してアプリを操作します。

Appiumを経由してアプリを起動する

Appiumのインストールはnpmを使います。npmの実行環境が入っていれば、以下の手順でAppiumをインストール、実行することが可能です。

$ npm install -g appium
$ appium

デスクトップ版も提供されているようなので、そちらを使うのも良いと思います。その場合、npmコマンドをインストールするなどは不要なので、軽く使ってみる、という話しでは十分かもしれません。

Appiumサーバを起動したら、以下のようにcapabilityを指定し、アプリを起動します。ここでは、サンプルとしてruby gemのappium_libを使った場合を記載します。Appiumサーバも以下のスクリプトも、同一の端末内にて実行する環境を想定しています。

require 'appium_lib'

IOS_CAPABILITIES = {
  automationName: 'appium',
  platformName: 'iOS',
  platformVersion: '7.0',
  deviceName: 'iPhone Retina (4-inch)',
  app: "cookpad.appへの絶対パス"
}.freeze

client = Selenium::WebDriver::Remote::Http::Default.new
client.timeout = 120

driver ||= Selenium::WebDriver.for(:remote,
                                   http_client: client,
                                   desired_capabilities: IOS_CAPABILITIES,
                                   url:'http://localhost:4723/wd/hub')
driver.manage.timeouts.implicit_wait = 30 # seconds
driver

※Appium 1.2.2時点では、Xcode6向けのinstrumentsには対応していないので、Xcode5をxcode-selectで指定しておく必要があります。 ※メソッドの使い方は、appium_libを完全に使っているわけでなありません。

ここまででアプリが起動するので、以降ではdriverに対して様々なメソッドをよび、シナリオを実施していきます。Appium自身が提供するチュートリアルも充実してきていますので、使い方の詳しくはそちらをご覧ください。

シナリオを書く

皆さんはシナリオを何で書きますか? 私は、今はTurnipを使いシナリオを記述しています。Cucumberの派生と捉えて頂ければ想像しやすいでしょうか。

例えば、シナリオを記述する.featureファイルには、以下のような自然言語でシナリオを記述します。

# encoding: utf-8
# language: ja

機能: 0000. ユーザはログインすることができる
  シナリオアウトライン: 0000.1
  ユーザはそれぞれ異なるアカウントでログインすることができる
    前提 <device> で試験を行う
    * <user_status> ユーザでログインする
    * 画面に <expected> が表示されている
    * スクリーンショットを <screenshot> という名前で撮る

  例:
    | device   | user_status |  expected    | screenshot |
    | 'iphone' | 'ゲスト'     | '期待する文字' |    '1'     |
    | 'iphone' | '無料'       | '期待する文字' |    '2'     |
    | 'iphone' | '有料'       | '期待する文字' |    '3'     |

それぞれのステップに対応した処理は別ファイルで実際の処理と関連づけます。この関連づけるためのファイルに、これらの操作をAppiumを経由して動作させるためのfind_elementsなどのメソッドを使ったスクリプトを記述します。ここでは特にTurnipの記述に関しては言及しません。

クックパッドにおける使い方

上記までのAppiumの使い方とシナリオの書き方を増やしていき、実際にどのようにしてそれらを使っているのかを記載します。

テストも開発プロセスの一部として実施する

言わずもがなですが、テストも開発プロセスの一部です。なので、何か修正を加えたら組み込まれたテストを常に実施し、健全な状態に保てるようにしたいです。一方、今回記載しているE2Eテストは、ユニットテストに比べて遥かに時間がかかり修正の度に実施、ということは難しいです。

現在のクックパッドのリリース間隔では、この自動テストを毎晩回すという進め方で十分なので、今は帰宅するときに実行して帰ります。リリース前にしか実行しないのではなく、頻繁に実行し、なるべく開発プロセスの一部になるように向けています。

機械が実行可能なタスクは機械に任せる

このようなテストを自動化する目的は、機械的に実施可能な箇所は機械に任せ、人はもっと振る舞いを観察しながらテストするところに力を注ぐことができるようにすることにあるかと思います。少ない人数で開発を回さないといけない開発現場は多いと思います。ならば、機械的に実施できる領域であれば機械に任せ、人がもっと別のことに時間を使えるようにする、というのは自然な流れですね。

人が忘れがちなシナリオは機械に任せる

例えば、検索結果がゼロになるときや、いくつかの状態がくみ合わさったときのみ再現する境界値のような状態など、人が実施すると忘れがちな箇所は機械に任せます。こうすることで、人の忘れによるテストケースの漏れを防ぐこともできますし、新規機能追加時のレイアウト崩れで不具合に気づくきっかけを得ることができます。

メンテナンス性

このようなテストケースはメンテナンスコストが膨れると言われます。私は、開発の活発さに合わせて適度にシナリオの統合やAppiumの変更にもある程度対応できるように薄いラッパー層をもうけるなどして最小限のメンテナンスで済むように調整しています。ここ数ヶ月の間、Appiumの更新にも、シナリオの調整にも片手間で追従できているので、今のところメンテナンスがかかりすぎるから使えない、という判断まではいっていないように思えます。

現在の実施状況

現在、iOSのクックパッド本体アプリに対してCheckingの文脈で定期的にこのE2Eテストを実施しています。IDを持つ無料ユーザ/有料ユーザ/IDを持たないユーザなどの組み合せによるシナリオの実施を異なるテストケースとした場合、テストケースは合計で100近くあります。これらをiPhone 4-inch/ iPhone 3.5-inch/ iPad向けに実施するので、パターン数では300を超える程度のシナリオセットを定期的に回していることになります。

iOSアプリデザインリニューアルの舞台裏で言及されていたスクリーンショットの話しでは、400枚近いスクリーンショットを毎晩取得し、翌朝、出社してから業務に入る慣らしとしてざっとスクリーンショットを確認していました。UIを変更する、という特性から、スクリーンショットを使い確認する、というシナリオ自体は非常に相性の良い流れでした。

今のところ、実施時間はすべてで2時間くらいです。夜間に回すにはまだ大丈夫そうです。特定のシナリオセットを実施する、ということも可能なので、これ以上が今は頑張っていません。

自動化テストのピラミッドを参考にするとほめられた構成ではないかもしれないですが、model以外のユニットテストが難しいモバイルアプリの現在のテスト自動化の環境からすると、現実的な姿ではないかと思います。

ROI?

私たちのリリース周期はだいたい約2週間に1回です。その期間の中では、1つ以上の何らかの不具合への気づきを与えてくれています。また、先で述べたシナリオすべてを人が実施すると、順調にいったと見積もっても1人日はかかると思います。そのように考えると、費用対効果、費用対便益という点では十分な価値を出していると思います。なお、テストスクリプトの作成自体、すべて合わせても恐らく1日かかってません。

また、ROI以上に、単純作業を機械に任せることで得られた時間やCheckingの時間短縮は非常に良いものでした。

最後に

テスト自動化に限った話しではないですが、様々な取り組みは自分たちの開発をよりスムーズにし、自分たちがやりたいことを実現するための手段だと思います。開発のスケールに対して検証のスケールはやはり現段階では難しいことが多いと思います。一方、自分たちに見合ったツールを組み合わせたりすると思いのほか現実的な形でスケールすることも可能かと思います。

いずれにせよ、自分たちに見合った開発/検証体制・プロセスを成長させ、やりたいことができる環境に一歩でも近づけると良いですね。

Android...

今回はiOSに関して記載しました。私たちはAndroidに関してもいくつかテスト自動化を実施しはじめています。 Appiumもuiautomator越しに特定のAndroidバージョンからサポートしていますが、その安定性はまだまだなので、adbコマンドを組み合わせるなどをしていくつか機械的に検出可能な障害性の高い不具合の検出をできるようにしています。そこらへんの取り組みの話しもどこかのタイミングでできれば幸いです。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/