XCTestによるUIテストでは、Toggleのタップがうまくいかず、ほとんどケースで失敗します。
ということで、なぜToggleのタップが失敗するのか?どうすればうまくタップさせることができるのか?について解説します。
さらに実用性を考えて、Toggleをうまくタップするための便利な拡張メソッドも紹介します。
UIテストでToggleのタップが失敗する原因
Buttonのタップは簡単
まずToggleのタップについて話を進める前に、ちょっともったいぶっておさらいとしてUIテストでButtonなどをタップする場合について触れておきます。
例として、ラベルが"ボタン"のButtonがあって、クリックされるとラベルが"ボタン押された"に変化するアプリについて考えます。
![](https://jimaru.blog/wp-content/uploads/2024/07/uitest-button-pressed.png)
プログラムはこんな感じ。
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があり、クリックされると"トグル押された"というラベルに変化するプログラムについて考えてみましょう。
![](https://jimaru.blog/wp-content/uploads/2024/07/uitest-toggle-pressed.png)
プログラムはこんな感じ。
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をタップするというアクションそのものは成功しています。しかしタップされている位置に問題があります。トグルがオンになるには、ラベル部分をタップしても駄目で、スイッチ部分がタップされる必要があるわけです。
![](https://jimaru.blog/wp-content/uploads/2024/07/toggle-switch.png)
↑この部分がタップされる必要がある
labelsHidden()
を使ってラベル部分を非表示にしたToggleの場合、上記のUIテストが成功します。toggle.tap()
によりスイッチがオンになり、ラベルが(UI上はHiddenで見えないけど)"トグル押された"に変化し、テストが成功します。
![](https://jimaru.blog/wp-content/uploads/2024/07/label-less-toggle.png)
ラベルが無いためコントロールのどこをタップしてもスイッチ部分だから、うまくいくワケですね。
ラベルが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)
![](https://jimaru.blog/wp-content/uploads/2024/07/cgvector-position.png)
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タップテストが必要なプロジェクトに遭遇するたびにXCUIElement
をextension
していくしかないのは嫌なので…誰かがライブラリ化してくれるか、既存のライブラリ知ってるよって方は、情報頂けるとありがたいです。