はじめに
iOSを開発し続ける中で、UITestを今までほぼ書いてこない開発人生を送ってきました。iOSDC2020でXCUITestのつらさを乗り越えて、iOSアプリにUITestを導入するの発表を見て、書いてみようと思い調べ実装したものをまとめました
開発環境
Xcode 12.4
Build version 12D4e
Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28)
Target: x86_64-apple-darwin19.6.0
PageObjectパターンとは
- PageObjectパターンは1つのページを抽象化したオブジェクトとして扱うパターンです。
- こちらに詳しく載ってました https://github.com/SeleniumHQ/selenium/wiki/PageObjects
実装してみた
発表のコードを参考にprotocolの定義します
import Foundation
import XCTest
protocol PageObjectable: AnyObject {
associatedtype Ally
var app: XCUIApplication { get }
init(app: XCUIApplication)
var exists: Bool { get }
var pageTitle: XCUIElement { get }
func elementsExist(_ elements: [XCUIElement], timeout: Double) -> Bool
}
extension PageObjectable {
var app: XCUIApplication {
return XCUIApplication()
}
var exists: Bool {
return elementsExist([pageTitle], timeout: 5)
}
func elementsExist(_ elements: [XCUIElement], timeout: Double) -> Bool {
for element in elements {
if !element.waitForExistence(timeout: timeout) {
return false
}
}
return true
}
}
操作したいUIに対して、accessibility identifier
をセット
- codeでもIBでもどちらでも設定できます
Page Objectを定義する
import Foundation
import XCTest
final class LoginPage: PageObjectable {
let app: XCUIApplication
init(app: XCUIApplication) {
self.app = app
}
var pageTitle: XCUIElement {
return app.navigationBars[Ally.title].firstMatch
}
var emailTextField: XCUIElement {
return app.textFields[Ally.emailTextField].firstMatch
}
var passwordTextField: XCUIElement {
return app.secureTextFields[Ally.passwordTextField].firstMatch
}
enum Ally {
static let title = "Log in"
static let emailTextField = "emailTextField"
static let passwordTextField = "passwordTextField"
}
@discardableResult
func typeEmail(_ email: String) -> Self {
_ = emailTextField.waitForExistence(timeout: 5.0)
emailTextField.tap()
emailTextField.typeText(email)
return self
}
@discardableResult
func typePassword(_ password: String) -> Self {
_ = passwordTextField.waitForExistence(timeout: 5.0)
passwordTextField.tap()
passwordTextField.typeText(password)
return self
}
func goToSuccessPage() -> SuccessPage {
let rightNavBarButton = app.navigationBars.children(matching: .button).firstMatch
_ = rightNavBarButton.waitForExistence(timeout: 5.0)
rightNavBarButton.tap()
return SuccessPage(app: app)
}
}
UITestを書く
- あとはテストしたい処理をXCTestCaseのクラスに書きます
final class XCUITest_SampleUITests: XCTestCase {
let app = XCUIApplication()
override func setUp() {
super.setUp()
continueAfterFailure = false
}
func testLogin() {
app.launch()
let page = LoginPage(app: app)
.typeEmail("test@example.com")
.typePassword("123456789")
XCTAssert(page.goToSuccessPage().exists, "successページに到達した")
}
}
はまったところ
文字入力のUITestが不安定
- Simulatarの設定を修正することで改善されます。
- Simulatarのkeyboardの設定を変更する
- Simulatar -> I/O -> Keyboard -> Connect Hardware KeyboardをOFFにする
-
https://stackoverflow.com/questions/55845373/disable-ios-simulator-connect-hardware-keyboard-programmatically
- pre actionとして登録しておくのも解決策です
UITestの実行が不安定
- Testが壊れていないのに失敗になるケースが発生しました
-
https://github.com/lyndsey-ferguson/fastlane-plugin-test_center
- fastlaneのpluginを使うと失敗時にRetryして、なるべく失敗扱いにしないようにした。
サンプルコード
- 実際に動くコードは下記にあります
- https://github.com/t-osawa-009/XCUITest-Sample
感想
- 発表を聞いた上で実装したことで理解が深まった。
- PageObjectパターンを採用することで画面に依存せずにUITestが書けるのでメンテしやすい印象を持ちました
- XcodeのUIテストが不安定な部分があるので対策が必要
- 既存のプロジェクトだといきなり全部に展開するのは難しいので小さいところから徐々に増やしていきたいです