161
154

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

UITestの導入とチートシート

Last updated at Posted at 2016-03-07

はじまり

Xcode7から導入されたUITestingですが、そろそろiOS9専用の機能も増えてきているし本格的に社内のプロジェクトで使ってみてもいいかなあなんて思っている方も多いのではないでしょうか。今回は導入から良く使うコードのチートシートをまとめてみました。つらいテスト作業を少しでも少なくするお手伝いになれば幸いです。

UITestの導入

ターゲットの追加

既存のアプリがObjective-Cで書かれているものでもUnitTestのようにSwiftでテストが書けます。迷わずSwiftで導入してください。こちらの記事が丁寧でわかりやすいかと思います。

導入時の注意点

上記の方法ですぐにテストが実行できれば問題ないのですが、私の環境では以下のようなエラーが出ました。

Undefined symbols for architecture x86_64:
"_main", referenced from:
     implicit entry/start for main executable
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

ひとつ目のエラーはアプリケーションテストを行う為のセットアップが済んでいないと出ます。 UnitTestのドキュメントXcode Unit Testing Guideに書いてありました。

  1. XcodeでUITestターゲットの「Build Setting」を開く
  2. スコープバーをAllにしてBundle Loaderに下記の値を入れる
 BUNDLE_LOADER = $(BUILT_PRODUCTS_DIR)/<app_name>.app/<app_name>
スクリーンショット 2016-03-04 19.17.47.png

ふたつ目のエラーはバイナリタイプのせいみたいです。よくわからない。デバッグとテストをバンドルに変更してあげると消えました。

  1. XcodeでUITestターゲットの「Build Setting」を開く
  2. Mach-O TypeのDebugに下記の値を入れる
MACH_O_TYPE = mh_bundle
スクリーンショット 2016-03-04 19.18.58.png

UITestの概要

XCUIApplication

こいつはアプリケーションそのものを示すクラスです。まずデフォルトで追加される$(PRODUCT_NAME)ファイルのsetUpメソッドを見てみましょう。

SomeAppUITests.swift
override func setUp() {
        super.setUp()

        // In UI tests it is usually best to stop immediately when a failure occurs.
        continueAfterFailure = false

        // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
        XCUIApplication().launch()
    }

XCTestCaseのプロパティであるcontinueAfterFailureはfalseを入れることでFailureだった場合そこで止まります。UnitTestではメソッド単位で独立したテストを行うのでデフォルトのtrueで使っていたと思いますが、UITestの場合一貫した画面操作なのでどこかでFailureになった時点で終了すべきなのだと思います。

次に出てきたXCUIApplication()はUITestで使用する最も重要なクラスで、テスト対象のアプリケーションそのものを指し、.launch()で起動しています。アプリの画面を操作する時はこのクラスが継承しているUIAElementを使って要素を探し出し、操作するといった記述をしていきます。

UIAElement

自動テストをする上でアプリケーションがもつボタンやテーブルビューなどの全てのユーザインタフェースの親クラス。こいつに存在確認をしたり、タップやスクロールなどの操作を加えることができます。

要素の特定

リファレンスを見れば分かる通り、アプリケーションから取り出せるの要素(UIKitのクラス)は通常UIAElementArrayというUIAElementの集合であり、その中から操作をしたいものを特定する必要が有ります。"UIA"は"UIAutomation"ですね。

一番単純な方法としては静的なテキストから要素を特定する方法です。ここにアプリケーションの画面上に"OK"と表示されているボタンがあります。

スクリーンショット 2016-03-05 22.33.56.png
// ボタンテキストから特定する(staticTextsは省略可能)
XCUIApplication().buttons.staticTexts["OK"]

テキストのない画像だけボタンの場合はAssetsに登録されている画像名でも特定が可能です。特定の方法はテキストと同じです。

スクリーンショット 2016-03-04 19.09.13.png
// 画像名から特定する
XCUIApplication().buttons["thmbs ok"]

しかしこれではテキストが可変だった場合や、画像名を変更した場合正常に動作しません。そこでIdentityInspectorにあるAccessibilityのIdentifierを設定することでテキストや画像名に代替して使用できます。

スクリーンショット 2016-03-04 17.26.55.png

コードの場合はボタンのインスタンスに対して下記のように指定します。あまり書きたくないですね。

thumbsButton.accessibilityIdentifier = "Thumbs Button"

特定の方法はテキストと同じです。

// Identifierから特定する
XCUIApplication().buttons["Thumbs Button"]

一番ハマったのはテーブルビューなどでセル内のContentViewにある要素を特定したい場合です。その要素自体にIdentifierが設定すれば良いのですが、動的に生成されるセルの中身に一々上記のコードを書くのは嫌です。

スクリーンショット 2016-03-05 22.54.07.png

ここで🍕のラベルを持つセルのテキストフィールドに値を入力するためタップする場合はこのようにしました。

// 子のビューのエレメントタイプから特定する
XCUIApplication().tables.cells.containingType(.StaticText, identifier:"🍕").childrenMatchingType(.TextField).element.tap()

また、個人的に面白いと感じたのですがXCUIApplicationは要素間の階層を自動的に補完してくれます。下記のコードは全て同じ動作をします。

// セルの中のボタンをタップする
XCUIApplication().tables.cells.buttons["Identifier"].tap()

// "Identifier"が画面内で要素を特定できる唯一のものであれば
XCUIApplication().buttons["Identifier"].tap()

// そのUIAElementが唯一のもの(ボタンが画面内にひとつ)であれば
XCUIApplication().buttons.element.tap()

チートシート

ここではテストで良く使うXCUIElementXCUIElementQueryなどのメソッドを紹介していきます。

要素やデバイスの操作

タップ

ボタンをタップ、セルをタップ、テキストフィールドにフォーカスするなどあらゆる場面で使います。ロングタップの場合は秒数を指定できます。

let button = XCUIApplication().buttons["Button"]

button.tap()
button.tapWithNumberOfTaps(5)
button.pressForDuration(5)

スワイプ

いい感じにスワイプしてくれます。テーブルビューの場合は1画面分スワイプしてくれる感じでした。

XCUIApplication().tables.cells["Cell"].swipeUp()

ドラッグ

pressForDuration:thenDragToElement:を使います。セルの入れ替えの場合は指定したセル要素の上までドラッグすれば入れ替わります。

let tables = XCUIApplication().tables
let topCell = tables.cells["Top Cell"]
let bottomCell = tables.cells["Bottom Cell"]

bottomCell.pressForDuration(0.5, thenDragToElement: topCell)

テキストを入力

上記のタップ操作をしてから入力することができます。

XCUIApplication().textFields["Name"].typeText("John Smith")

インデックスで指定

XCUIApplication().tables.elementBoundByIndex(0)
XCUIApplication().tabBars.elementBoundByIndex(1)
XCUIApplication().toolbars.elementBoundByIndex(2)

スライダーの操作

XCUIApplication().sliders["Percentage"].adjustToNormalizedSliderPosition(0.5)

ピッカーの操作

XCUIApplication().pickerWheels["Age"].adjustToPickerWheelValue("25")

複数のピッカーが並んでいる場合は少し複雑になります。ここで出てくるNSPredicateはデータの抽出などで使用する条件(クエリ)を作成できるクラスです。構文についてはこちらを参照してください。あまり理解できていないです。

// "First Picker"で前方一致という条件
let firstPredicate = NSPredicate(format: "label BEGINSWITH 'First Picker'")
let firstPicker = app.pickerWheels.elementMatchingPredicate(firstPredicate)
firstPicker.adjustToPickerWheelValue("first")

// "Second Picker"で前方一致という条件
let secondPredicate = NSPredicate(format: "label BEGINSWITH 'Second Picker'")
let secondPicker = app.pickerWheels.elementMatchingPredicate(secondPredicate)
secondPicker.adjustToPickerWheelValue("second")

ウェブビューのリンクタップ

XCUIApplication().links["Top"].tap()

リフレッシュ操作

リロードなどをする時に行う引っ張ってリフレッシュする操作はSpriteKitで使用するCGVectorMake()pressForDuration:thenDragToCoordinate:を使用します。最小値は6[m/sec]だそうです。

let cell = XCUIApplication().tables.cells["Cell"]

// 縦方向に重力ゼロと6[m/s]のXCUICoordinateを生成
let start = cell.coordinateWithNormalizedOffset(CGVectorMake(0, 0))
let finish = cell.coordinateWithNormalizedOffset(CGVectorMake(0, 6))

// 上記の重力がかかるまで引っ張る
start.pressForDuration(0, thenDragToCoordinate: finish)

アラートが出た時の処理

アラートが出た時の為に事前に処理を登録しておくことができます。

// 位置情報の許可アラートが出た時の処理
addUIInterruptionMonitorWithDescription("Location Services") { (alert) -> Bool in
  alert.buttons["Allow"].tap()
  return true
}

// 地図画面に移動して上記の処理を実行
XCUIApplication().buttons["Map"].tap()
XCUIApplication().tap()

キーボードの操作

テキストフィールドなどでキーボードが出ている状態で使えます。buttonskeysが別になっているのが気持ち悪いですね。ちなみにapp.keyboardsが通常の指定の仕方です。

スクリーンショット 2016-03-06 0.46.05.png
let app = XCUIApplication()

app.buttons["Return"].tap()
app.buttons["Next keyboard"].tap()
app.keys["more, letters"].tap()
app.keys["more, numbers"].tap()
app.keys["delete"].tap()
app.keys["shift"].tap()
app.otherElements["good"].tap()
app.buttons["Hide keyboard"].tap()

デバイスの操作

// ホームボタンを押下
XCUIDevice.sharedDevice().pressButton(XCUIDeviceButton.Home)

// デバイスを左回転
XCUIDevice.sharedDevice().orientation = .LandscapeLeft

要素のテスト

存在確認

UITestの中で一番基本になるテストだと思います。要素が存在すればtrueが返ってきます。

// "Button"のIdentifierを持ったボタンが存在するか
XCTAssert(XCUIApplication().buttons["Button"].exists)

// プッシュ先でどの画面かナビゲーションバーのタイトルで確認する
XCTAssert(app.navigationBars["Title"].exists)

表示確認

上記のexistsだと画面外で表示されていてもtrueが返ってきてしまします。ここで画面にちゃんと表示されているかを確認するにはCGRectContainsRect()を使用します。

let app = XCUIApplication()

// ウィンドウ(UIKitだとUIWindow)を取得
let window = app.windows.elementBoundByIndex(0)

// ボタンがウィンドウのフレーム内に含まれているか
let element = app.buttons["Button"]
XCTAssert(CGRectContainsRect(window.frame, element.frame))

非同期処理

読み込み画面が出るなどの非同期処理が走る場合にある条件を満たすまで待つことができます。ピッカーで出てきたNSPredicateを使っています。

// くるくる画像がいなくなるまでという条件
let waitingImage = self.app.images["Waiting"]
let notExists = NSPredicate(format: "exists == false")

// 検索してくるくるがいなくなるまで5秒間処理を中断する
expectationForPredicate(notExists, evaluatedWithObject: waitingImage, handler: nil)
app.buttons["Search"].tap()
waitForExpectationsWithTimeout(5, handler: nil)

ちなみにwaitForExpectationsWithTimeoutで指定した時間内に条件を満たさなかった場合Failureとなりテストが終了します。

その他

デバッグ

シミュレータ内でGeneral > Accessibilityの Accessibility Inspectorを使うと要素のIdentifierやフレームなどを確認できます。

スクリーンショット 2016-03-06 2.03.46.png スクリーンショット 2016-03-06 2.04.35.png

上記を立ち上げると操作が面倒なので、ちょっと見たい時などはコンソールに出力することもできます。

print(XCUIApplication().buttons["Button"].debugDescription)

おしまい

ここまでズラズラと書いてきましたが、Identifierがしっかりと設定されていればチートシートにある大部分はレコーディング機能を使用すれば自動で記述してくれます。日本語ダメだったり、結構わけわからないことするのでリファクタリングは必須ですが…
あと機能の改修を行った際にはほぼ間違いなくUITestのコードも改修が必要になると思われます。この修正に当たるリソースと人力で行うテスト作業のリソースを比べてうまく運用していかないと本末転倒なことになりそうです。今後のアップデートに期待したいと思います。

参考文献

UIAElement Class Reference
UIAElementArray Class Reference
XCTest Reference
UI Testing Cheat Sheet
UI Testing Cheat Sheet and Examples

161
154
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
161
154

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?