👾

XCTest UI Testとも仲良くなる

2025/02/19に公開

Swift Testingと仲良くなるでSwift Testingとは仲良くなれた気がしたので引き続きで、UI Testとも仲良くなりたいと思います。

公式サイト

https://developer.apple.com/documentation/xctest/user-interface-tests

UIテストの基本形

大きな流れは以下のとおりです。

  1. XCUIApplicationによりアプリ起動する
  2. 起動したアプリに対して、XCUIElementQueryを使用してUI要素(XCUIElement)を見つける
  3. XCUIElementに対して実施したい操作(クリックやタップなど)を実行する
  4. 操作した結果に対してXCTestアサーション(Swift Testing)を利用して評価を実施する

つまり、以下の3つの関数を利用するのがベーシックな流れです。

テストコードの基本形

continueAfterFailureはテスト失敗時に処理を止めるかの設定で、falseは止めずに評価を続ける設定になります。

CalclatorUITests.swift
import XCTest

final class CalculatorUITests: XCTestCase {

    override func setUpWithError() throws {
        continueAfterFailure = false
    }

    override func tearDownWithError() throws {
    }

    @MainActor
    func testExample() throws {
        let app = XCUIApplication()
        app.launch()
    }
}

UIテストを実行してみる

実際に基本形でUIテストを実行してみましょう。まずは、UIテストの対象となるMainViewを以下の要領でコーディングしていきます。
ポイントは、UIElementごとにaccessibilityIdentifier(String)で設定することで、どのUIコンポネントにアクセスするか を明確にしています。

UIテストの対象を整備する

以下のソースコードをテストしていきます。
UIは、テキストエリアと、0から9までのボタンが横並びする非常にシンプルなものです。

MainView.swift
import SwiftUI

struct MainView: View {
    @Binding var input: String;
    
    var body: some View {
        VStack{
            TextField("入力してください", text: $input)
                .disabled(true)
                .foregroundStyle(.white)
+                .accessibilityIdentifier("calc_textfield")
                .padding()

            HStack {
                ForEach((1...9), id: \.self) { num in
                    Button("\(num)", action: {
                        input.append(String(num))
                    })
+                    // XCUITestでUIコンポネントに直接アクセスできるよう名前をつける
+                    .accessibilityIdentifier("button_\(num)") 
                    .frame(width: 45, height: 45)
                }
            }

        }
        .padding()
    }
}

#Preview {
    @Previewable @State var text: String = "";
    return MainView(input: $text)
}

テストコードを書く

新しくtestButtonTap関数をUIを評価する関数にします。

CalculatorUITests.swift
import XCTest

final class CalculatorUITests: XCTestCase {
    
    override func setUpWithError() throws {
        continueAfterFailure = false
    }

    override func tearDownWithError() throws {
    }

    // 追加コード
    @MainActor
    func testButtonTap() throws {
        let app = XCUIApplication()
        app.launch()
        
        // TextFieldのUI要素を取ってくる
        let textfield = app.textFields["calc_textfield"]
        
        // 0から9までのボタンをタップ(クリック)する
        for idx in 0...9 {
            let button = app.buttons["button_\(idx)"]
            button.tap()
        }
        
        // TextFieldの値が期待する値(0123456789)になっているか検証する
        XCTAssertEqual(textfield.value as! String , "0123456789")
    }
}

テスト実行中

XCodeが自動でUI起動、クリックなどオートメーションで実行されます。

テスト結果

XCTestSwift Testingを同じく、エラーの場合はソースコードエラーと同じ要領で、ソースコードが赤くハイライトされます。

XCUITestの使い方・主要関数一覧

よく使うケースを網羅的にまとめました。主に自分用メモなので辞書的な使い方を考えています。

iPhone/iPad端末を操作する

iOS、iPadOSのテストの際に、デバイス自体を操作する関数が用意されています。
XCUIDevice.sharedで現在使われているエミュレータの情報を取得して操作します

使い方
let device = XCUIDevice.shared
// デバイスの向き
device.orientation = .landspaceLeft
// 物理ボタンの動作
device.press(.home)
関数 用途
press(_:) 端末の物理ボタンを押す。.action.camera.home.volumeUp.volumeDownの5種類が設定可能

UI要素を選択する

大きく以下の関数が提供されています。
使い方はXCUIApplicationまたはXCUIElementを返す変数に対して処理します

使い方
let app = XCUIApplication()
app.launch()

let buttons = app.children(matching: .button)
関数 用途
children(matching: ) マッチする子要素を返す
descendants(matching: ) マッチする子孫を返す
accessibilityIdentifier()を指定したUI要素を取得する

UIイベント関連

大きく以下の関数が提供されています。
使い方はXCUIElementQueryを返す変数に対して処理します

使い方
let button: XCUIElementQuery = app.buttons["my_identifier"]
button.tap()
関数 用途
tap() 1本の指で1回押す動作を行う
doubleTap() 1本の指で2回押す動作を行う
twoFingerTap() 2本の指でタップする動作を行う
swipeLeft() 左にスワイプする動作を行う
swipeRight() 右にスワイプする動作を行う
swipeUp() 上にスワイプする動作を行う
swipeDown() 下にスワイプする動作を行う
pitch() 2本の指で広げる動作を行う
rotate(_:withVelocity:) 2本の指で回転する動作を行う

UIのライフサイクルを操作する

大きく以下の関数が提供されています。
使い方はXCUIApplicationを返す変数に対して処理します

使い方
let app: XCUIApplication = XCUIApplication()
app.launch()
関数 用途
launch() アプリケーションを起動する
activate() アプリケーションを再起動する(iOSアプリのようにサスペンドしている)
tarminate() アプリケーションを終了する

テスト結果を永続化する

XCTAttachmentを使います。

使い方
func testAttachment() {
    let attachment = XCTAttachment(string: "ここにテスト結果の文字列などを格納する")
    attachment.name = "MainView" 
    attachment.lifetime = .keepAlways // テスト成功時も記録に残す
    add(attachment)
}

テスト結果のキャプチャを取る

XCUIScreenshotXCTAttachmentを組み合わせて使います。
キャプチャファイルはReportビュー(Command+9)で確認できます

使い方
func testTakeScreenshots() {
    let app = XCUIApplication()
    app.launch()
    let windowScreenshot = app.windows.firstMatch.screenshot()

    let attachment = XCTAttachment(screenshot: windowScreenshot)
    attachment.name = "MainView" 
    attachment.lifetime = .keepAlways // テスト成功時も記録に残す
    add(attachment)
}

Discussion