iOSのUITestについて


この記事は Goodpatch Advent Calendar 2015 10日目の投稿です。
昨日は @Haseda による エディタ初心者がSublimeTextでHTML5/CSS3を書く環境を作るのに苦労した話 でした!


はじめまして、Goodpatchでエンジニアをしています @HirokiTerashima です。

今日はiOS開発におけるUITestのお話をしようかと思います。

UIテストって?

名前の通り、UIを触ってテストすることです。所謂、結合テストに近いかもしれませんね。
これを人力でやるのも良いのですが、何かを変更する度にテストするのも大変ですよね。
それを触らずとも勝手に動かしてもらって楽しようぜってお話です。

iOSのUIテストツール

  • Xcode(UIRecording)
  • KIF
  • Appium
  • Calabash iOS

などがあります。
この中で特に注目なのがXcode7から追加されたXcodeのUIRecordingですね。
果たしてこれがどんな感じのものなのか、KIFと比較して見ていきたいと思います。

KIFについて

KIFのソースはこちら
有名なオープンソースのUITestライブラリです。最初に触った時は感動しました……本当に自動で動くんだ〜と。

ただし、非公開のAPIを使っているとのことでテスト用のターゲットのみに含めておかないとリジェクトを喰らいます。

確かに便利なのですが、少々使いにくい点が幾つか……

AccessibilityLabelをつける問題
VoiceOverで読み上げしてもらうためにつける属性なのですが、KIFではタップする要素を指定するときにこのAccessibilityLabelを指定します(AccessibilityIdentifierでも大丈夫です)。
つまり既に結構出来てしまっているアプリに対してKIFでテストを書くときは、ボタンやらいろんなものにAccessibilityLabelをつけていかなくてはなりません。これは結構な労力がかかりますね……

一応AccessibilityLabelをつけなくても、ボタンのテキストなどを指定しても問題ありませんが、テキスト内容を変更する度にテストコードも変更しなくてはならないので面倒ですね。

AccessibilityLabelの付け方はStoryboardから付ける方法とコードからつける方法があります。

もしくは

sample.swift
let button = UIButton()
button.accessibilityLabel = "buttonLabel"

因みにAccessibilityLabelを指定できないもの(アラートやタブバーのボタンなど)はボタンのテキストを指定すると操作できます。

システムアラート問題
アプリ初回起動時などに出てくる、プッシュ通知を許可しますか?とかカメラのアクセスを許可するかなどを聴いていくるアラートのことです。
これらのアラートのボタンを押すコードを書くときは acknowledgeSystemAlert というメソッドを呼ばないとタップしてくれません。出るタイミングを把握していないとこれでテストが失敗します。
しかもこのメソッド、シミュレータだけでしか動かないです。

テスト失敗時、次のテストに移るとき問題
テストはテストケース毎にメソッドを分けると思うのですが、操作対象の要素が表示されていなくてテストに失敗し、次のテストに移るとき画面は前のテストで失敗した時のままになります。
それだと後続のテストがすべて失敗する可能性があります……

対処としては KIFTestCase クラスの beforeEachafterEach などのメソッドでテスト開始時毎、もしくはテスト終了毎に popToRootViewControllerAnimated などを利用して画面を最初に戻す処理を書いておく必要があります。

ダブルタップの操作が面倒くさい
座標を指定してタップする、という操作を2回連続で行うように書きます。
AccessibilityLabelを指定してタップを2回書いても1回目と2回目で時間がありすぎてダブルタップの認識になりません。

let point = CGPointMake(100, 30)
tester.tapScreenAtPoint(point)
tester.waitForTimeInterval(0.1)
tester.tapScreenAtPoint(point)

UIRecording

さてXcode7から追加されたこの機能ですが、起動したアプリを操作するとその操作内容のコードが自動で書かれて行くという優れものです。

こんな感じになります。

操作をする度にXcode上のソースがどんどん追加されていますね。便利!
しかし操作を記録してそのままテスト実行してもテスト失敗することが多々あります……操作対象の要素の取り方が間違っていたり、変なコードを生成したり……
またスワイプやスクロールなどは記録してくれません。
TableViewをシミュレーター上でスクロールしてもセルをタップしたとしか認識してくれません。

そういった場合、手動でテストコードを書き換えていく必要があります。

各種操作

基本は操作対象の要素を取得して、そのオブジェクトから操作メソッドを呼び出します。

let app = XCUIApplication()
// 取得は操作対象のテキスト、もしくはAccessibilityIdentifierを指定します
let label = app.staticTexts["text or identifier"] // UILabel
let button = app.buttons["text or identifier"] // UIButton
let textField = app.textField["text or identifier"] // UITextField
let table = app.tables.elementBoundByIndex(0) // UITableView
let cell = app.tables.cells.staticTexts["text"] // UITableViewCell
let cell2 = app.tables.cells.elementBoundByIndex(0) // UITableViewCell
let alert = app.alerts["title"] // UIAlertController
let slider = app.sliders["identifier"] // UISlider

// 各種操作
button.tap() // ボタンタップ
textField.tap() // テキストフィールドタップ
textField.typeText("HogeFuga") // テキストフィールド入力
table.swipeUp() // テーブルスクロール
cell.tap() // セルタップ
alert.buttons["title"].tap() // アラートボタンタップ
slider.adjustToNormalizedSliderPosition(1) // スライダースライド

// 存在確認
XCTAssertTrue(app.tables.cells.staticTexts["text"].exists)

// 要素が存在するまで待つ
let element = app.buttons["text or identifier"]
let existsPredicate = NSPredicate(format: "exists == true")
expectationForPredicate(existsPredicate, evaluatedWithObject: element, handler: nil)
waitForExpectationsWithTimeout(10, handler: nil)

色々な操作や要素の取得方法があるので詳しくはドキュメントなどを参照してください。

KIFと比べて……

テストが失敗しても再開が楽ちん
setUp() を見るとわかるのですが、テストメソッド毎にアプリが起動しなおしてくれます。

override func setUp() {
    super.setUp()
    continueAfterFailure = false
    XCUIApplication().launch()
}

なので止まってもまた最初からテストが再開されるので、KIFみたいに自前で管理する必要がないので良いですね!

KIFだと面倒くさい操作用のメソッドが準備されている
doubleTap twoFingerTap tapWithNumberOfTaps:numberOfTouches: など操作に関するメソッドが分かりやすくかつ豊富。

結局AccessibilityIdentifierが必要……
テキストを指定してもよいのですが、変更された時にテストも変更するのが嫌な場合はAccessibilityIdentifierのほうが良いですね。
なので早いうちにテストは書いておこうということですね。

システムアラートは?
出た時に操作を書くKIFとは違って、予め出た時の操作を書いておく感じになります。

addUIInterruptionMonitorWithDescription("System Alert") { (alert) -> Bool in
    alert.buttons["Allow"].tap()
    return true
}

やっぱりできないことも……
Safariに飛ばされた後の操作などがやはりできません(WebViewは操作可能です)

最後に

やはり一長一短なところがありますね。
しかしざっくり書くにも実際にアプリを操作しながらできるのは速いですし、テスト失敗時の再開が楽など利点も多いかなと思います。
最初はUIRecordingでざっくりとテストを書いて、細かい調整を自前で書くという感じになりそうです。

まだまだこれからなものだと思うので今後に期待して、動向を注意深く見ていきたいと思います!


さて、明日はGoodpatchのエンジニア @aoshi によるGitHubとSlackとの連携に便利なHubotプラグインのお話です。
乞うご期待!