Qiita で知り合った方との勉強会で発表(しようとして全然間に合わず帰ってから実装)した UI テスト自動化の話。
iOS のイベント処理の仕組み
日本語はこちら→ https://developer.apple.com/jp/devcenter/ios/library/documentation/EventHandlingiPhoneOS.pdf
黒魔術
iOS において、ユーザーが画面をタップしたり、デバイス自体を傾けたりシェイクしたり、別のリモコンを使って音楽を再生したりといった「行為」は、UIEvent
というオブジェクトにまとめられ、それを UIView
や UIViewController
や UIGestureRecognizer
がハンドリングすることで、「行為」に対する「アクション」を実現している。
UI のテストを自動化しようと思うと、まずこの UIEvent
のモックを作って、それを例えば UIView
の touchesBegan:withEvent:
に渡すことでタッチイベントを模倣することが考えられる。だだし iOS は UIEvent
をものごっつカプセル化しており、タッチイベントの実体である UITouchesEvent
(UIEvent
の孫クラス)に至っては公開すらされていない。
それでも無理やり自動化しようと思うと、これはもう黒魔術を使わざるを得ない。Objective C の場合、全てのメッセージパッシングは id
型に対して行われるため、非公開のメソッド(例えば UITouchesEvent
の _firstTouchForView:
)であっても、Mock クラスに同名のメソッドを用意してやれば問題なく動かすことが出来る。ただし、内部実装はいつ変更されるかわからないため、このテスト手法が将来にわたって使えることは保証できない。
サンプルコード
流れとしては、
-
window
(UIWindow
インスタンス) に対してタップした場所point
(CGPoint
)を渡し、最初にタッチイベントをハンドリングできるview
を探す(hitTest:withEvent:
を呼び出す) - 取得した
view
に対して、touchesBegan:withEvent:
(タッチ開始)及びtouchesEnded:withEvent:
メソッドを呼び出す。 - これで画面上の
point
をタッチし、離したというイベントがview
に渡り、それがボタンであること、期待する処理が行われること、をテストできる。
@interface AMOUITestViewControllerTests : XCTestCase
@end
@implementation AMOUITestViewControllerTests
- (void)window:(UIWindow *)window tapSimulationWithPoint:(CGPoint)point
{
// UIEvent のモック
MockUIEvent *event = [MockUIEvent new];
// 当たり判定
UIView *view = [window hitTest:point withEvent:event];
// UITouch のモック
MockUITouch *touch = [MockUITouch new];
touch.aTouchPoint = touch.aPreviousTouchPoint = [view convertPoint:point fromView:nil];
touch.aWindow = window;
touch.aView = view;
touch.aTapCount = 1;
NSSet *touches = [NSSet setWithObject:touch];
event.aTouches = touches;
// タッチ開始
touch.aPhase = UITouchPhaseBegan;
[view touchesBegan:touches withEvent:event];
// タッチ終了
touch.aPhase = UITouchPhaseEnded;
[view touchesEnded:touches withEvent:event];
}
- (void)testCase
{
UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 568)];
AMOUITestViewController *vc = [AMOUITestViewController new];
window.rootViewController = vc;
[window makeKeyAndVisible];
CGPoint touchPoint = CGPointMake(vc.view.center.x, vc.view.center.y / 2);
// 最初の色は vc.deactiveColor である
XCTAssertEqualObjects(vc.someView.backgroundColor, vc.deactiveColor);
// ボタンをタッチした
[self window:window tapSimulationWithPoint:touchPoint];
// タッチ後の色は vc.activeColor である
XCTAssertEqualObjects(vc.someView.backgroundColor, vc.activeColor);
// もう一度タッチした
[self window:window tapSimulationWithPoint:touchPoint];
// タッチ後の色は vc.deactiveColor に戻っている
XCTAssertEqualObjects(vc.someView.backgroundColor, vc.deactiveColor);
}
@end
github
以下でサンプルコードを公開している。
https://github.com/amo12937/AMOUITestApplicationSample/tree/20141214_for_qiita
git clone -b 20141214_for_qiita git@github.com:amo12937/AMOUITestApplicationSample.git
マイルストン
スワイプ操作の模倣
touchesMoved:withEvent:
を使えば、スワイプなどもきっと模倣できる。ただしどんな 非公開メソッドを使っているかはわからないので MockUIEvent
や MockUITouch
には修正が必要かもしれない。
ライブラリ化
テストは確かに黒魔術だが、アプリバイナリとして申請する際にはテストコードは含まれないので、これをもってリジェクトされることは無いはず。ただし、黒魔術を使ったテストを信じるべきではない、という意見は確かにその通りで、受託案件では特に、これを持って結合試験の代わりにはできないだろう(参考程度にはなるかも)。ご利用は飽くまで自己責任で。