Swift

SwiftのUIテストでToggleのタップに失敗する場合の対策

2024-07-03

XCTestによるUIテストでは、Toggleのタップがうまくいかず、ほとんどケースで失敗します。

ということで、なぜToggleのタップが失敗するのか?どうすればうまくタップさせることができるのか?について解説します。

さらに実用性を考えて、Toggleをうまくタップするための便利な拡張メソッドも紹介します。

UIテストでToggleのタップが失敗する原因

Buttonのタップは簡単

まずToggleのタップについて話を進める前に、ちょっともったいぶっておさらいとしてUIテストでButtonなどをタップする場合について触れておきます。

例として、ラベルが"ボタン"のButtonがあって、クリックされるとラベルが"ボタン押された"に変化するアプリについて考えます。

プログラムはこんな感じ。

import SwiftUI

struct ContentView: View {
    @State var buttonLabel = "ボタン"
    
    var body: some View {
        List {
            Button(buttonLabel) {
                buttonLabel = "ボタン押された"
            }
        }
    }
}

そしてUIテストでは、単純にbtn.tap()とすることでタップでき、そのラベルが"ボタン押された"に変化することがテストで観測できます。

import XCTest

final class UITestSample: XCTestCase {

    @MainActor
    func testExample() throws {
        let app = XCUIApplication()
        app.launch()
        
        let btn = app.buttons["ボタン"].firstMatch
        btn.tap()
        
        let tappedButton = app.buttons["ボタン押された"].firstMatch
        XCTAssert(tappedButton.exists)
    }
}

Toggleのタップは失敗する

ではToggleの場合はどうか?

例として、"トグル"というラベルのToggleがあり、クリックされると"トグル押された"というラベルに変化するプログラムについて考えてみましょう。

プログラムはこんな感じ。

import SwiftUI

struct ContentView: View {
    @State var on = false
    
    var toggleLabel: String {
        var toggleLabel = "トグル"
        if on { toggleLabel += "押された" }
        
        return toggleLabel
    }
    
    var body: some View {
        List {
            Toggle(toggleLabel, isOn: $on)
        }
    }
}

そしてこれをUIテストで単純にtoggle.tap()としても、Toggleはオンになりません。

import XCTest

final class UITestSample: XCTestCase {

    @MainActor
    func testExample() throws {
        let app = XCUIApplication()
        app.launch()
        
        let toggle = app.switches["トグル"].firstMatch
        toggle.tap()
        
        let tappedToggle = app.switches["トグル押された"].firstMatch
        XCTAssert(tappedToggle.exists) // ->XCTAssertTrue failed
    }
}

Toggleのタップが失敗する原因

先ほどのプログラムでは、おそらくToggleをタップするというアクションそのものは成功しています。しかしタップされている位置に問題があります。トグルがオンになるには、ラベル部分をタップしても駄目で、スイッチ部分がタップされる必要があるわけです。

↑この部分がタップされる必要がある

labelsHidden()を使ってラベル部分を非表示にしたToggleの場合、上記のUIテストが成功します。toggle.tap()によりスイッチがオンになり、ラベルが(UI上はHiddenで見えないけど)"トグル押された"に変化し、テストが成功します。

ラベルが無いためコントロールのどこをタップしてもスイッチ部分だから、うまくいくワケですね。

ラベルがHiddenでないToggleのタップを成功させる方法

実際のアプリ開発ではラベルを非表示にするわけにはいかないので、ラベルを表示させつつUIテストでも適切にスイッチ部分をタップしたいですよね。

そのためには座標を直接指定してタップします。toggle.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5))としてToggleの座標を取得し、座標に対し.tap()をコールします。

import XCTest

final class UITestSample: XCTestCase {

    @MainActor
    func testExample() throws {
        let app = XCUIApplication()
        app.launch()
        
        let toggle = app.switches["トグル"].firstMatch
        let toggle.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5))
        coord.tap()
        
        let tappedToggle = app.switches["トグル押された"].firstMatch
        XCTAssert(tappedToggle.exists) // OK
    }
}

ポイントはwithNormalizedOffsetに指定するCGVectorの値です。

Toggleの中心座標はCGVector(dx: 0.5, dy: 0.5)で表されますが、オン/オフを切り替えるスイッチ部分が位置する右端のほうの座標を取得する必要があります。つまり、CGVector(dx: 0.9, dy: 0.5)とするのが正解です。

Toggleをタップするためのメソッドを定義

.coordinate(withNormalizedOffset:)で取得した座標を.tap()することでToggleに対するUIテストができることは確認できました。

でも、これをUIテストで何度も記述するとテストがごちゃごちゃして可読性が低下してしまいますよね?何度も繰り返す記述は、専用メソッドにしてしまいましょう。

専用メソッド

extensionを使ってXCUIElementを拡張します。タップする位置をオフセットで指定できるtap(offsetX:offsetY:)と、右端のほうのオフセットをタップするtapRightSide()を定義して、可読性を保ちましょう。

extension XCUIElement {
    func tap(_ offsetX: Double, _ offsetY: Double) {
        coordinate(withNormalizedOffset: CGVector(dx: offsetX, dy: offsetY)).tap()
    }
    
    func tapRightSide() {
        tap(0.9, 0.5)
    }
}

これを活用するとUIテストは次のようになります。

final class UITestSample: XCTestCase {
    @MainActor
    func testExample() throws {
        let app = XCUIApplication()
        app.launch()
        
        let toggle = app.switches["トグル"].firstMatch
        toggle.tapRightSide()
        
        let tappedToggle = app.switches["トグル押された"].firstMatch
        XCTAssert(tappedToggle.exists) // OK
    }
}

extensionライブラリ

GitHubのプロジェクトからインポートするだけでtap(offsetX:offsetY:)func tapRightSide()が使えるようになるライブラリを作成しようとしましたが、Swiftのテストターゲット以外はXCTestがリンクされておらず、うまくいきませんでした。

ハックすることでライブラリ作成も可能っぽいものの、難易度高くて沼にハマりそうだったのであきらめ。

Toggleタップテストが必要なプロジェクトに遭遇するたびにXCUIElementextensionしていくしかないのは嫌なので…誰かがライブラリ化してくれるか、既存のライブラリ知ってるよって方は、情報頂けるとありがたいです。

-Swift

© 2024 ヂまるBlog