はじめに
ユニットテスト環境でUI部分のテストをするためのテクニック集です。
SUT (UIView, UIViewController)の設定方法
UIViewをそのまま生成
let view = UIView()
windowのルートビューコントローラーに設定
シミュレーターに画面が出る。実行しているシミュレータの端末に関わらず、画面サイズを変更可能。
let window = UIWindow(frame:CGRect(origin: .zero, size: CGSize(witdh: 375, height: 667)) // 画面サイズを指定できる 例 4インチ:CGSize(witdh:320, height: 568)
window.rootViewController = hogeViewController
window.makeKeyAndVisible()
画面のサイズを帰る。
シミュレーターに画面が出る。
let window = UIWindow(frame:CGRect(origin: .zero, size: CGSize(witdh: 375, height: 667)) // 画面サイズを指定できる
画面の処理を進ませる。
Runloopでイベントループを回す。
WIP
UIイベントの発火させかた。
イベントハンドラーを直呼び
実装コストは低いがアクセス権限を壊す必要がある。
hogeView.hogeButtonTapped()
XCTAssertEqual(......
コードによるUI操作+デレゲートメソッド直呼び
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .left)
// プログラムアブルな操作はデレゲードメソッドが呼ばれないので直呼び
collectionView.delegate?.collectionView!(collectionView, didSelectItemAt: indexPath)
XCTAssertEqual(......
sendActionsメソッドを使う。
xibの設定が正常に行っているかまでテストできる。
button.sendActions(for: .touchUpInside)
XCTAssertTrue(....)
リフレクション:perform系メソッドを呼ぶ
プライベートスコープでも呼べる。ただし、メソッド名を間違えるとテスト実行時にエラーが発生する。
viewController.perform(NSSelectorFromString("hogeButtonTapped"))
XCTAssertEqual(......
検証方法
プロパティを外だし。
実装コストは低いがアクセス権限を壊す必要がある。なるだけgetterのみ出す。Objectiv-Cはゴニョゴニョするとテスト環境のみ外だしできる。
XCTAssertEqual(cell.commentLabel.text, "hope")
画面のスナップショットを取る。
テストが壊れなければ楽。そう壊れなければ
FBSnapshotVerifyView(vc.view)
Accessibility IdentifierでSubviewを外だし。
プライベートスコープでも呼べる。ただし、失敗すると実行エラーが発生。xibから設定する場合なぜか中々反映されない。コードから反映した方が良い?
let cell = vc.view.viewMatchingAccessibilityIdentifier("タイトル") as! HogeTableViewCell
XCTAssertEqual(cell.titleLabel.text, "hope")
JSONベースでマッチング
内部の状態をJSON文字列で出力して検証する。返って手間がかかる?
WIP
ダミーのNavigationControllerを作成して画面遷移の挙動を検証する。
表示させるとテストの実行時間が伸びるので寸止めさせる。
// テストスパイ化したUINavigationController
final class TestSpyableNavigationController: UINavigationController {
private(set) var pushedViewController: UIViewController?
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
// コンストラクター呼び出し時とpush時の2回通るため、その対応。
if viewControllers.count == 0 {
super.pushViewController(viewController, animated: animated)
} else {
pushedViewController = viewController
}
}
}
// テストコード
...
viewController.perform(NSSelectorFromString("hogeButtonTapped"))
XCTAssertTrue(nc.pushedViewController!.isKind(of: HogeViewController.self))
まとめ(になっていない)
- 参考文献1にあるようにUI丸ごとテストする場合は、テスト駆動開発というよりかはデバック駆動開発になる。
- 実行時エラーが発生しやすい手法しか取れない点でViewModelを使うよりかは効率が落ちる。とはいえ、その分設計コストは小さいのでトントン??。
- テスト駆動でプロパティ検証->デバック駆動でスナップショットという流れでやるとそれなりの効率が上がるかも。
- ROIに応じて適切な手法を採用することが肝要。
- Android Test FrameworkみたいにUIを単体テスト、統合テストの両方を切り替えるだけで行える機構が今後でてくるかも。
参考文献/スライド
(1) [App Architecture: iOS Application Design Patterns in Swift] (https://www.amazon.co.jp/dp/B07D21KRJN)
(2) iOS Code Testing: Test-Driven Development and Behavior-Driven Development with Swift (English Edition)
(3) さわって学べる!iOSテスト駆動開発
(4)[Viewのテストどうしてますか?] (https://speakerdeck.com/kariad/viewfalsetesutodousitemasuka)
(5) [Snapshot Testing in iOS] (https://speakerdeck.com/susieyy/snapshot-testing-in-ios)