XCTestを使ったUIテストの安定化戦略 – STORES Product Blog

XCTestを使ったUIテストの安定化戦略

Hello! STORES レジ を開発している@AkkeyLabです!

Swift Testingで実装可能なユニットテストと異なり、UIテストは従来通りXCTestを利用して実装を行います。そのため、ここで紹介する個々の手法に目新しさを感じない人も多いでしょう。しかし、iOSアプリでUIテストを継続運用している事例はユニットテストと比べて少ない印象で、記事にする価値があると考えました。そこで、今回は STORES レジ に導入中のUIテストを安定化する中で得たノウハウを紹介します。

この記事を読むことで、次に示す学びを得ることができるでしょう。

  • 不安定なUIテストの原因を切り分けられるようになる
  • Xcode Cloudのレポートでテストの失敗シーンを正しく特定できる
  • テストの実行環境に依存しないテストの実装方法が理解できる

不安定原因の推定

まずはどのように不安定であるか、現象を分析する必要があります。次に、私がよく遭遇すると感じている事例を示します。3つ目はタイムアウト値が極端に大きかったり、タイムアウト設定が欠損していることによるものなので、最終的には1もしくは2の現象であることが大半です。

  1. ローカルではテストが成功するが、CI上では失敗する
  2. コードや環境に変更がなくても、テストに成功する場合と失敗する場合がある
  3. テストに失敗はしないが、いつまで経ってもテストが終わらない

では、このような現象の根本的な原因としてどのようなことが考えられるのでしょうか。ユニットテストではなくUIテストであるという点にフォーカスして、考えられる事例を次に示します。

サービスが地域制限を受けている

Xcode CloudをはじめとしたCIサービスを活用している場合、そのUIテストは日本以外の地域で動作している可能性が高いです。そのため、サービスを利用できる地域を限定している場合には、手元のXcodeではテストに成功するのに、CI上だと失敗するという現象になる可能性があります。

WebViewの読み込み遅延や失敗

ネイティブ実装部分とWebView内部とでレンダリングサイクルは大きく異なります。そのため、通信環境や読み込み先サーバーの応答次第では、再現率の低い不安定な失敗が発生します。

UI表示の未完了状態で操作している

UI要素がまだ画面に表示されていない、もしくは操作可能な状態になっていないうちに操作を試みているケースです。UIKitやSwiftUIのレンダリング処理には多くの最適化が施されており、瞬間的に「タップできないけど要素は存在する」という状況が生まれることがあります。また、記事の後半で紹介する「要素がキーボードに隠れてしまう」場合もこれに該当します。

Xcode Cloudを活用した調査

ここではXcode Cloudにフォーカスして効率的にテストの失敗原因を調査する方法を紹介します。ユニットテストであればWeb上のコンソールだけでもある程度調査しやすいですが、UIテストの場合はXcodeに統合されたXcode Cloudを利用するのが便利です。まず、次に示す手順に従ってXcodeからXcode Cloudに接続してください。

XcodeからXcode Cloudに接続する様子

  1. Xcodeで、該当のアプリを管理するApp Store Connectに紐づいたApple IDでログイン
  2. Xcodeの左端にあるメニューからReport Navigatorを選び、Cloudを選択
  3. 該当のビルドを選ぶ

Xcode Cloudでのテスト結果を閲覧できるようになったら、次に示す手順に沿ってテスト失敗の原因箇所を特定していきます。これは実際に私が不安定なテスト失敗に遭遇した時のものです。

XcodeでUIテストの失敗原因を調査する様子
XcodeでUIテストの失敗原因を調査する様子

  1. 左のメニューから「Tests」を選択
  2. テスト実行中に収録された動画が閲覧可能で、ここから意図しない挙動をしている箇所を探します
  3. 原因箇所を特定します。今回は「ログイン」ボタンが描画されていないにも関わらず、ボタンが発見されてしまい、次の処理に進んでしまっています
  4. 動画再生エリアでは、タップを試みた位置を確認できます

このように調査することで、今回はボタンのタップ操作が正しく動作していなかったことを特定できました。なお、この調査方法はWWDC23のFix failures faster with Xcode test reportsで紹介されていますので、併せてご覧ください。

安定した実装のためのテクニック

ここからは、先ほどの調査で発覚したボタンのタップミス問題を解決していきます。結論としては、ボタンという操作対象の要素が「存在」していて、尚且つ「操作可能」になるまで待機してからタップ処理を実行することで解決します。では、どのようにその判定と待機を実装に落とし込めばよいのでしょうか。

let app = XCUIApplication()
app.launch()

let element = app.buttons["Login"]
if element.exists, element.isHittable {
    element.tap()
}

要素が存在するか、操作が可能であるかの判定処理は上記のように実装できます。existsisHittableXCUIElementのプロパティです。しかし、この実装はほぼ意味をなしません。なぜなら、意図した状態になるまで待機する処理が含まれていないからです。ほとんどの場合、タップされることなくテストが失敗することでしょう。

では、どのように待機させるとよいでしょうか。XCTestからはXCTWaiterというクラスが提供されており、このクラスのwait関数が利用できます。これを使った実装例を次に示します。

let app = XCUIApplication()
app.launch()

let element = app.buttons["Login"]


let predicate = NSPredicate(format: "exists == true AND hittable == true")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)


let result = XCTWaiter.wait(for: [expectation], timeout: 30)

XCTAssertEqual(result, .completed, "Element should exist and be hittable within 30 seconds")

if result == .completed {
    element.tap()
}

wait 関数にはXCTestExpectationで期待値を指定する必要があるため、NSPredicateで待機条件を定義しています。ここで使われているプロパティは XCUIElementexists および isHittable に対応します。 NSPredicate の条件式では KVC のルールにより isHittable プロパティを hittable というキー名で参照できます。この点をもう少し具体的にみてみましょう。

まず、KVCとはKey-Value Codingの頭文字をとったもので、プロパティ名を文字列で指定して値を取得・比較できる仕組みです。
NSPredicateの評価では、オブジェクトに対して内部的に value(forKeyPath:) を呼び出して値を取得します。そのため、条件式で指定するプロパティ名は KVC でアクセス可能なキーでなければなりません。実際に有効なプロパティ名を確認する処理を次に示します。

#keyPath(XCUIElement.exists) 
#keyPath(XCUIElement.isHittable) 

#keyPathはSwiftコンパイラが提供する「Key-pathリテラル」と呼ばれる構文で、型安全にKVC用の文字列を生成します。このKey-pathリテラルを活用すると、次に示すように型安全な条件式に書き換えることができます。

let predicate = NSPredicate(
    format: "%K == true AND %K == true",
    argumentArray: [
        #keyPath(XCUIElement.exists),
        #keyPath(XCUIElement.isHittable)
    ]
)

ソフトウェアキーボードの影響

UIテストで意外と見落としがちなのが、キーボードの存在です。キーボードが画面下部のボタンやテキストフィールドを覆ってしまうと、その要素を操作できない状態になってしまうことがあります。その対策方法を次に示します。

要素が見えるまでスクロールする

操作対象の要素が操作可能な領域に来るまでスクロールするステップを追加する方法です。先ほど紹介した existshittable を活用し、対象の要素が操作可能になる領域までスワイプする処理を実装します。

キーボードを閉じる

iPadでソフトウェアキーボードを閉じる操作の候補

画面の設計次第では、何かしらの手段でキーボードを一度閉じる必要がある場面も考えられます。次に示すように、閉じ方にも複数の方法があるため、状況に合わせた最適な方法を選択する必要があります。

  • Returnキーで閉じる(改行できる場合は不可)
  • キーボードを閉じるボタンで閉じる(iPadの場合)
  • 入力エリア外をタップして閉じる
  • アプリに独自実装したボタンで閉じる

UI要素がキーボードに隠れるかどうかは、テスト端末の画面サイズによって変わります。例えばiPhone SEサイズでは隠れてしまうが、iPhone 17 Proでは隠れない、というような差が出ることもあります。サポートする端末の中で、最も小さな画面を搭載した端末を使ってテストケースを実装するのも一つの選択肢です。


UIテストは「書いたら終わり」ではなく、安定して動くようにメンテナンスすることが大切です。そして、安定したUIテストはCIの信頼性を高め、開発チーム全体の生産性を支える大きな力になります。

ここで紹介した内容が1つでも日頃の開発の助けになれば幸いです。
最後まで読んでいただき、ありがとうございます。

STORES ではエンジニアを絶賛募集中です。 ぜひ採用サイトにも遊びに来てください!

jobs.st.inc




元の記事を確認する

関連記事