5.1さらうどん

@giginetの技術ブログ。ゲーム開発、iOS開発、その他いろいろ

Tuneup JS+Travis CIによるiPhoneアプリ自動UIテストまとめ

最近、iPhoneアプリの開発に自動UIテストを取り入れてみたので、手に入れた知見を共有してみたいと思います。

この記事について

iOSアプリケーションの自動UIテストを行うためのノウハウについて解説します。

この記事におけるUI自動テストとはiOSシミュレーターや実機を自動で起動し、予め記述していたとおりに操作させ、アプリケーションが問題なく動いているかどうかをテストする手法のことです。

今回はTuneup JSと呼ばれるライブラリを用いて、アプリの自動再生、要素のチェック、画像比較によるテストを行い、最終的にTravis CI上で動かすところまでを書いています。

iOS開発の知識のほか、JavaScript, Rubyを知っていると良いかも知れません。

ここで紹介するもののいくつかはRuby製であり、RubyGems, Bundler, Rakeなど、最低限のユーティリティが動く・使える環境を想定しています。

また、Unit Testingについては、下記の資料が素晴らしく良くできているのでご参照ください。

Cocoa 勉強会関西で Unit Test について話しました - cockscomblog?

TuneupJSって?

http://www.tuneupjs.org/

Apple謹製のUITesting FrameworkにUIAutomationというものがあります。
UIAutomationでは、テストケースをJavaScriptで記述できます。

大変良くできているのですが、単体では利用しづらい部分が多いです。それを補うユーティリティ集がTuneupJSです。

例えば、UIAutomation単体で行うのは面倒だったAssertionなどのテストの記述が、より他のBDDフレームワークライクに記述できるようになります。

ここでTuneupJSを利用した場合と、していない場合のコードを比較してみます。

before
UIALogger.logStart("Checking the navigation bar right button title");
var target = UIATarget.localTarget();
var title = target.frontMostApp().navigationBar().rightButton().name();
if (title != "Books") {
  UIALogger.logError("Expected 'Books' but was '" + title + "'");
  UIALogger.logFail("Some tests failed"); // failed
} else {
  UIALogger.logPass("Tests passed"); // passed
}
after
test('Checking the navigation bar right button title', function(target, app) {
  assertEquals(target.frontMostApp().navigationBar().rightButton().name(), "Books");
});

例えば、上記のコードは、ナビゲーションバーの右側のボタンのタイトルをテストするテストケースです。

このように記述量に大きく差が出ます。

TuneupJSの導入

TuneupJSはCocoaPodsで導入可能ですが、バージョンが古いため、submoduleで入れた方が良いと思います。

git submodule add https://github.com/alexvollmer/tuneup_js.git

TuneupJS+UIAutomationでテストを書く

TuneupJSは、UIAutomationを拡張するものであるため、基本的なテストの記述はXcodeに付属しているInstruments上で行えます。

Instruments

Instrumentsを使うと、iOSシミュレーターの操作から自動的にJavaScriptのコードを生成してくれます。

あなたのiOSプロジェクトをProfileビルドしましょう。Instrumentsが起動するのでAutomationメニューを開くと、以下のような画面が立ち上がります。

f:id:gigi-net:20130918002257j:plain

左カラムからJavaScriptを新規作成し、画面下の録画ボタンを押してiOSシミュレーターを動かしましょう。自動的にJavaScriptが生成されます。

f:id:gigi-net:20130917214444j:plain

基本的にこれをコピペするだけで、動作を自動再生することができるのですが、実際にはアニメーションのタイミングなどでdelayが必要です。

target.frontMostApp().navigationBar().rightButton().tap();// トランジションが入るから
target.delay(1.0); // 1秒待っておく
assertEquals(target.frontMostApp().mainWindow().staticTexts()[0].label(), "foobar");
 // そうしないと表示されるまえに評価されてテストが落ちたりすることがある

サンプルコード

例えば、ここでは、ボタンを50回連打したときに表示されるラベルが正しいかどうか、というテストを書きました。人手で50回叩くのはムダっぽい感じがするので、こういうことは自動化しましょう。

#import "../../tuneup_js/tuneup.js"
test('Tap button fifty times', function(target, app) {
  target.frontMostApp().mainWindow().buttons()[0].tapWithOptions({tapCount:50});
  target.delay(1.0);
  assertEquals(target.frontMostApp().mainWindow().textViews()[0].value(), "Pressed 50 times");
});

テストを走らせる

TuneupJSにはテスト実行用の簡易なスクリプトがついています。今回はこのスクリプトを利用してテストを走らせてみましょう。

テストの実行には

  • xcodebuildを用いてアプリケーションをビルドする
  • TuneupJS付属のスクリプトを走らせる

と言った手順が必要です。今回は簡易なRakefileを実装し、UIテストを行う一連の流れをタスク化してみました。

以下、Rakefileの例を示します。


gist6595415

細かい説明は省略しますが、要はテストを実行するには

tuneup_js/test_runner/run <実行ファイル.app> <Spec.js> <出力用ディレクトリ>

を呼び出してあげればOKです。詳しくは以下のドキュメントをご参照下さい。

http://www.tuneupjs.org/running.html


上手く行くと、下記動画のように自動的にシミュレーターが起動し、記述したとおりの動作が実行されるはずです。

http://gifzo.net/2wbLx1aLwL.gif
http://gifzo.net/GLdCEQz880.gif

Screenshot Assertion

TuneupJSの特徴の一つにScreenshot Assertionが挙げられます。Screenshot Assertionは、予め想定されるiOSスクリーンショットを用意しておき、実行時に自動的に撮影されたスクリーンショットと比較して、画像間の差異が一定以下であればテストを通す仕組みです。

これを使うことで、画像1枚用意するだけで、表示崩れなどをおおざっぱに判定することができます。

仕組みとしては、内部でImageMagickに付属しているcompareを利用しており、その出力の値によって判定しています。
そのため、実行にはImageMagickの導入が必要です。

brew install imagemagick
Pull Requestしました

僕が使い始めた時点でのTuneupJSは、ImageAssertionの画像比較結果の許容値が固定値であり、ほぼ同じ表示であるにも関わらず、テストが通らないケースが続出しました。(例えばカーソルの点滅やインジケーターのアニメーションの差異などで)

そのため、Pull Requestを送り、テストケース毎に画像の誤差の許容値を指定できるようにしました。
これは既にmasterにmergeされており、現在のバージョンのTuneupJSで利用可能です。

Add argument for assertScreenMatchesImageNamed to set diff threshold. by giginet · Pull Request #57 · vkolgi/tuneup_js · GitHub

テストケースの記述

まず、テスト用の正解画像を用意します。このとき、ステータスバー上20px分を切り取ったスクリーンショットである必要があります。

f:id:gigi-net:20130917235511p:plain

次にテストケースを書きます。こちらなどを参照に書くといいです。
http://www.tuneupjs.org/assertions.html

  createImageAsserter('../../tuneup_js',  './',  '../../specs/image');
 // ImageAsserterの初期化
// 何らかの操作を行う
  assertScreenMatchesImageNamed("tap_text_view", "a screenshot of text view is not matched.", 1500); // 比較

createImageAsserterの引数のパスはinstruments実行時に生成されるRunディレクトリからの相対パスを書くようです。

また、assertScreenMatchesImageNamedの第3引数は、僕の送ったPull Requestから追加された引数で、画像比較の差異を指定することができます。デフォルトの値(引数を渡さない場合)は1になっているようです。

閾値はとりあえずデフォルト値に設定しておいて、テストが落ちた場合は

compare --metrics MAE <教師画像> <テストが落ちたときのスクリーンショット画像> diff.png

と実行し、手前の数字を参考に決めると良いです。

Travis CIで動かす

手元の環境で動くようになったので、今風っぽくCIしたいですね。ご自宅のJenkins氏で走らせるようにしてもよいですが、なんと嬉しいことに、全く同じテストが全てTravis CI上で動きます。

実はTravis CIには、Mac workerが存在し、Objective-Cのテストが行えます。

xcodebuildでiOSアプリのビルドができるのはもちろん、特に設定せずともHomebrewが利用できるほか、今回紹介したシミュレーターの起動さえもTravis CI上で行えます。

.travis.ymlの設定方法

Travis CIでプロジェクトをCIするためには、リポジトリルートに.travis.ymlというYAMLを配置しておく必要があります。

このように書くと上記のテストと同じモノがTravis CIで走ると思います。

language: objective-c

before_script:
  - brew update
  - brew install imagemagick

script:
  - rake

before_scriptでImageMagickのセットアップを行っており、スクリーンショットの比較もTravis CI上で行えます。

Travis CIの環境では、テストの実行毎にクリーンな状態にロールバックされるため、テストの実行の度にImageMagickをビルドしていることになります。実に富豪的

UIテストは通常の操作と同じように行うため、非常に時間がかかるのですが、20~30分は動かしても怒られないと聞いたことがあるため、問題なくテストが実行できます。

f:id:gigi-net:20130918000129j:plain

Travis CI - Test and Deploy Your Code with Confidence

これだけ負荷をかけても無料で実行可能。良い時代ですね。

ここでは特に解説しませんが、TestFlight APIなどを使って、テストが通ったら自動的にTestFlightを飛ばすようにしてもおもしろそうです。

サンプルプロジェクト

今回、紹介した物は以下のリポジトリに大体は含まれているので、ご参照ください。

GitHub - giginet/AutomationTestDemo: This is a demo project for ui testing with TuneupJS

その他便利ユーティリティ

bwoken

GitHub - bendyworks/bwoken: iOS UIAutomation Test Runner

UIAutomation用テストランナー。導入するとRakeを書いたりせずに、コマンド一発でテストが走ってお便利。

TuneupJSのドキュメントではこれの利用が推奨されている。
さらに、通常のUIAutomationに加えて、以下のような機能がサポートされている。

  • 元々Rakeのタスクが定義されていてrakeコマンド一発でテストが走る
  • テストケースがCoffeeScriptで記述でき、テスト時に自動コンパイルしてくれる
  • 外部ライブラリをgithubから直接インポートできる

こう聞くと、かなり良い感じのライブラリに見えるけど、これ単体では微妙な部分が多い。

確かに普通に自動テストを走らせる分にはTuneupJS付属のスクリプトより相当楽だけど、カスタマイズ性が低く使い勝手が悪いのが残念。例えば、projectの代わりにworkspaceを使ったり、schemaやtargetを変えたり、シミュレータを変えたりといったことが内部に手を加えないとできない

それと、テストケースをCoffeeScriptで書くのも微妙な感じです。確かに嬉しいけど、前述の通りInstrumentsがJSのコードを自動生成してくれるので、書き直すのもなんだかかっこ悪い。

さらに1年近く保守されていないのもマイナスポイント。だれかforkして直すと良いと思います。

Nocilla

GitHub - luisobo/Nocilla: Testing HTTP requests has never been easier. Nocilla: Stunning HTTP stubbing for iOS and Mac OS X.

Objective-C製のstubbingライブラリ。例えば「Twitterクライアントを作りたいけどテストしにくい!」なんて時に、表示部分だけであれば、Requestをstubして、ダミーのデータを返してやればテストしやすいです。

ただ、若干機能面で使い勝手がよろしくないですが、これ以上に優れたものも見つからなかったので、上手い感じで使えば良いと思います。

ios-sim

GitHub - ios-control/ios-sim: Command-line application launcher for the iOS Simulator

iOSシミュレーター用のCLI。これでシミュレーターをコマンドライン上でを起動、制御できる。

TuneupJSには現状、シミュレーターを制御する機構がないので、例えば4インチ環境でテストしたい、なんて時に、これでシミュレーターを切り替えてやる必要があります。

Homebrewで入って導入が簡単なのも嬉しい。

brew install ios-sim
mechanic

GitHub - jaykz52/mechanic: A CSS-style selector engine for iOS UIAutomation

UIViewをCSSセレクタ風に取得できるようにするライブラリ。TuneupJSなどと一緒に使えます。

正直、Viewの構造がわかりにくいし、Instrumentsを使ってGUIで選択した方が早い気がするので使ってません。良さそうだったら教えて下さい。

その他のTesting Framework

本稿で紹介したUIAutomationの他にも、iOSのUIテストフレームワークには、Objective-Cで記述できるKIFや、Cucumberで記述できるcalabash-iosなど、様々なUI Testing Frameworkがあります。

GitHub - kif-framework/KIF: Keep It Functional - An iOS Functional Testing Framework
GitHub - calabash/calabash-ios: Calabash for iOS

KIFは最近のバージョンでさらに良くなったみたいですね。あんまり使ってないのでよくわかりません。

calabash-iosは試してみたのですが、CucumberのSpecをDSLみたいなので書かないといけないので微妙に使い勝手が良くないです。
せっかくCucumberを使っているので、calabash-iosでFeatureは日本語を使って書けないか試してみたんですけど、現時点でi18nには対応していないそうです。

まとめ

手間がかかるので、ここまで厳密にテストすることはないと思うのですが、Travis CI上で動き出すとなかなか嬉しいですね。

アプリの開発以外にも、例えばCocoaControlsにUIコンポーネントを提供するときも、Travis CIのバッヂがリポジトリ上に表示されているとカッコイイ感じです。

スクリーンショットによるアサーションは凄そうな技術に見えるけど、あんまり期待しない方が良くて、ざっくりとした表示の確認にのみ使い、挙動などは要素をチェックする二段構えでテストした方が良さそう。

手前味噌ですが、iOSのUIテストの手法がこれだけまとまってる記事は日本語はもちろん、世界中探しても皆無だったので、書いて良かったと思います(小並感)。少しでも多くのiOSデベロッパーのお役に立てば幸いです。
がんばって書いたので、気に入ったらはてブ+シェアをしてくれたら嬉しいです。

機会があれば、同内容を札幌のiPhone開発コミュニティであるdevsapでお話しできたら良いなあと考えています。

チラシの裏

9月21日は僕の誕生日です。23歳になります。

Amazon.co.jp