59
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

XcodeのUIテストフレームワーク「XCUITest」のTips

XCUITestでUIテストの自動化にチャレンジしています。
その中で得られたTipsを記します。

環境

Xcode 10.1
Swift 4.2

そもそも、XCUITestとは

  • Xcodeに統合されているUIテストフレームワークです。
  • シナリオコードを書いて、ユーザーの操作をシミュレーションします。
  • アプリの挙動が期待通りになっているかのアサーションも、もちろんコードで記述します。
  • アサーションに失敗したタイミングで、自動的にスクリーンショットを撮ってくれます。
  • ブレークポイントで止めておいて、操作の記録を開始すると、操作をテストコードに自動変換してくれます(この「レコーディング機能」の注意については後述)。
  • Xcode 10より"Parallel Testing"つまり並列実行が導入され、パフォーマンスが向上しました。

導入手順と、基本的なテストコードの書き方

本稿では割愛します。
以下の記事が参考になるかと思います。
【Swift】初めてのUITest導入
UITestの導入とチートシート
UI Testing Cheat Sheet and Examples
※少々前の記事になりますが、基本は変わっていないと思います。

メリット/デメリット、そして導入のポイント

XCUITestの導入を検討されている方向けに、先にまとめっぽいことを書きます。
※個人の感想です。

メリット

一般的なコトではありますが…

  • 書いてしまえば、リグレッションテストを自動化できる。
  • JenkinsなどでCIに組み込める。
  • UIのデグレード防止に一定の効果があり、リファクタリングする"勇気"が出る。
  • テストのドキュメントを書かなくても済む。
  • したがって、アジャイルとは相性が良い(はず)。

デメリット

  • 慣れないうちはテストを書くことにコストがかかる。
  • リーダブルなテストコードを書くようにチーム内で統一意識を持たないと、テストコードのメンテナンスにコストがかかる。
  • Parallel Testingが導入されたとはいえ、実行時間はそれなりに掛かる。
    • テストコードが数百件になると、Parallel Testingでも1時間ぐらい掛かると思います。

ポイント

  • XCUITestは書いていて楽しいけど実行時間が掛かるので、「書き過ぎ」に注意。
    • ロジック部分はXCTestで書いた方がContextも分かりやすいし実行時間も短い。
    • XCUITestはUI部分のテストだけと割り切った方が良い。
  • ということは、アプリのアーキテクチャ検討の際、ロジックとUIを分離させる戦略が必要ということになります。
    • アーキテクチャ検討の際は、テスト戦略やCIも踏まえて検討を!
  • テストコードをリーダブルに保つ重要性をチーム全員が良く理解することが大事!
    • 具体的には、テスト対象、テスト条件、そして期待結果(= Context)が分かるように書くこと

XCUITestは効果的に使うと、テストを楽しめると思います!:wink:

Tips

ここからが私なりの知見です。

利用範囲をあらかじめ決めておくべし

  • XCUITestはユーザー操作をシミュレーションするモノなので、結合テスト的に「も」使えます。
  • 画面単独のUIテストにのみ利用するのか、画面間の結合にも利用するのか、チーム内で利用範囲の認識を合わせておいた方が良いと思います。

テストコードの作成単位をあらかじめ決めておくべし

  • 上とも関連しますが、テストメソッドやファイルをどのような単位で作成するのか、あらかじめチームの決め事を作っておかないと、カオスになります。
  • 画面単独であれば、"テスト対象クラス名UITests.swift"で事足りますが、画面間の結合にも利用するとなると、どのファイルにどんなシナリオが含まれるのか、さっぱり分からなくなります。

レコーディング機能は極力使うべからず

  • レコーディング機能は、必ずView(テスト対象の項目)を取得できるとは限らず、エラーになる場合もあります。
  • レコーディング機能に頼りすぎると、上手くViewを取得できない場合に時間を浪費します。
  • レコーディング機能で自動生成されるコードからはContextが読み取りにくく、テストコードのメンテナンスにコストが掛かります。

accessibilityIdentifierを活用すべし

  • どのようにしてViewを取得するのが良いかというと、accessibilityIdentifierを設定するのが良いです。
    • UIButtonなど、テキストを持っているViewであれば、app.buttons["ほげ"]などでも簡単に取得できるのですが、UIImageViewなどはaccessibilityIdentifierを設定しないと取得しづらいです。
    • また、『「ほげ」を「ふが」に文言変更する』なんていういことも多々ありますし…

コードで設定する場合はこんな感じで書きます。
button.accessibilityIdentifier = "ViewController_button"

StoryBoardであれば、"Identity inspector"にて設定します。
スクリーンショット 2018-12-16 6.56.50.png

XCUITestではこんな感じで書きます。
app.buttons["ViewController_button"]

設定する文字列は、上のように「クラス名_View名」などルールを決めると、テストコードのContextを読み取りやすいと思います。
(単にapp.buttons["button"]とすると、Contextが分かりにくくなります。)

XCTContext.runActivity(named:)でテストのContextを表現すべし

  • 前述の通り、テストのContextを読み取れることは重要です。
  • コメントに書くのも悪くないですが、どうせなら、XCTContext.runActivity(named:)にテスト条件や期待結果を記述する方が良いです。
  • XCTContext.runActivity(named:)に書くと、以下のようにXcodeのReport navigatorに表示されて見やすくなります。

スクリーンショット 2018-12-15 17.38.27.png

「待つ」コードがポイント

  • 「何かのタイミングで通信を開始して、インジケータを表示して操作禁止とし、レスポンスが返ってきたら画面操作を可能にして…」とか、よくあると思います。
  • 「表示されるまで待つ」とか「tapできるようになるまで待つ」という場面は結構頻繁にあり、待たせ方がポイントになります。
    • 固定的な時間で待たせると、環境によって待ち時間が足りなかったり、実行時間が余分に掛かってしまったりします。
  • 後述するサンプルコードのwaitToAppear()waitToHittable()を参照してください。

共通メソッドでAssertする場合は、引数に#fileと#lineを設定すべし

  • そうすることで、共通メソッドの呼び出し元の行がAssertion Failureになります。
  • 逆にそうしないと、共通メソッド内でAssertion Failureになってしまい、失敗原因箇所がわかりにくくなります。
  • 後述するサンプルコードのwaitToAppear()waitToHittable()を参照してください。

通信のスタブと組み合わせるべし

テストデータを投入しやすいローカルDB

  • Realm SwiftはXCUITestに向いていると思います。
    • JSONからデータを簡単に投入できます。
    • メモリー上に簡単にインスタンスを生成できます。

環境変数などを上手く利用して、XCUITestからの実行の場合は通信やDBをスタブ化するようにすると捗ります。

サンプルアプリ

ここからはサンプルコードによる解説です。
まず、テスト対象のサンプルアプリです。

Qiita201812.gif

UIButtonがtapされたらActionSheetを表示して、その選択結果によってUILabelにテキストを表示する、という、ごくシンプルなものです。

プロジェクトはGitHubに公開しておきました。
https://github.com/y-some/UITestSample

サンプルテストコード

import XCTest

class ViewControllerUITests: XCTestCase {
    let app = XCUIApplication()

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

    override func tearDown() {
    }

    func testExample() {
        XCTContext.runActivity(named: "初期表示でTextViewの文言を確認") { (activity) in
            waitToAppear(for: app.textViews["ViewController_textView"])
            XCTAssertEqual(app.textViews["ViewController_textView"].value as? String,
                           "Hello world!\n\nXCUITestのサンプルです。")
        }
        XCTContext.runActivity(named: "ボタンをtap、ActionSheetのAction1をtapし、Labelの文言を確認") { (activity) in
            waitToHittable(for: app.buttons["ViewController_button"]).tap()
            waitToAppear(for: app.sheets["タイトル"])
            app.buttons["Action1"].tap()
            XCTAssertEqual(app.staticTexts["ViewController_label"].label,
                           "Action1がtapされました")
        }
        XCTContext.runActivity(named: "ボタンをtap、ActionSheetのAction2をtapし、Labelの文言を確認") { (activity) in
            waitToHittable(for: app.buttons["ViewController_button"]).tap()
            waitToAppear(for: app.sheets["タイトル"])
            app.buttons["Action2"].tap()
            XCTAssertEqual(app.staticTexts["ViewController_label"].label,
                           "Action2がtapされました")
        }
        XCTContext.runActivity(named: "ボタンをtap、ActionSheetのCancelをtapし、Labelの文言を確認") { (activity) in
            waitToHittable(for: app.buttons["ViewController_button"]).tap()
            waitToAppear(for: app.sheets["タイトル"])
            app.buttons["Cancel"].tap()
            XCTAssertFalse(app.staticTexts["ViewController_label"].exists)
        }
    }
}

extension XCTestCase {
    func waitToAppear(for element: XCUIElement,
                      timeout: TimeInterval = 5,
                      file: StaticString = #file,
                      line: UInt = #line) {
        let predicate = NSPredicate(format: "exists == true")
        let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
        let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
        XCTAssertEqual(result, .completed, file: file, line: line)
    }

    func waitToHittable(for element: XCUIElement,
                        timeout: TimeInterval = 5,
                        file: StaticString = #file,
                        line: UInt = #line) -> XCUIElement {
        let predicate = NSPredicate(format: "hittable == true")
        let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
        let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
        XCTAssertEqual(result, .completed, file: file, line: line)
        return element
    }
}
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
59
Help us understand the problem. What are the problem?