iOSのテストってぶっちゃけおざなりだったわけですが、サーバサイドはちゃんとテスト書いてるのにクライアントサイドだけ書かないとだんだん品質が不安になってくるわけです。
これまでも多少なりとも努力してきたつもりですが、iOSの場合、テスト書いといて良かったみたいなケースがこれまでまるでなくて、むしろAPIの動作確認のため書くみたいな。個人的に非常にしょぼい状況だったわけです。
ただXcode5でTest周りが強化され、かつ個人的にしっくり来るテストフレームワークの組み合わせが見つかったので、これでテンション上がってバリバリテスト書ける気がしてきたのでまとめてみました。
テンション上がらないと何にもできないっすもんね!
KIFって
あのカード決済のSquareが自社向けに作ったテストフレームワークです。
https://github.com/kif-framework/KIF
特徴としては以下です。
- ボタンをタップした、などのUIアクションベースでテストが書けて実行できる。
- Objective-Cでかける
- 2.0より、Xcode5といい感じに連携して実行ができる(例 : Command+Uなど)
- 実機でも動かせる。
この動画見たほうが早いかもですね。 (ちょっと古いですが)
http://corner.squareup.com/2011/07/ios-integration-testing.html
なんか、世間一般では、MVCでいうところのModel周辺をまずはしっかり書くべきとなってるようですが、ぶっちゃけiOSなんでほとんどのバグがUI起因だったりします。そのためModelよりなによりControllerの正常系ルートをさくっとテストできたほうがよっぽど安心するわけです。なので、UnitTest系のテストフレームワークより、KIFのようなフレームワークのほうがよっぽど重宝します。(僕だけかも)
NLTHTTPStubServerって
HTTP通信用のスタブサーバです。iOSのローカルにサーバを立てて、アプリからの通信を受け取り、登録したレスポンスを返却してくれるものです。なんか通信系のテストといえば、モックを作って差し込んでテストするとか、差し込みやすくするようにプロダクトコードを書くとかいろいろあると思うのですが、正直めんどくさいです。ボクには向いてません。もうこっちのほうがよっぽど便利です。
PreProcessor Macroあたりを利用して、テスト実行時のみアクセス先をローカルに向けてしまえばNLTHTTPStubServerがよしなにレスポンスを返却してくれるので、特にテストを意識せずプロダクトコードを書けちゃうわけです。非常にいいです。
作成者の Ukyoさんに感謝です!
http://yaakaito.org/blog/2013/04/22/release-nlt-http-stub-server-0-4-0/
KIFのインストール
実は少し手こずりました。まとめます。
プロジェクトの作成
適当に新規プロジェクトから、SingleViewApplicationを選択し、プロジェクトを作成します。
今回は、KIFPracticeとしました。またこの時同時にKIFPracticeTestsというTargetも作成されます。
KIFのInstall
Podfileを作成します。KIFとNLTHTTPStubServerはテストプロジェクトのみ依存のため、以下のとおりexclusive => true を設定します。
platform :ios, '7.0'
target :KIFPracticeTests, :exclusive => true do
pod 'KIF', '~> 2.0'
pod 'NLTHTTPStubServer'
end
インストールを実行します。
> pod install
Analyzing dependencies
CocoaPods 0.26.2 is available.
Downloading dependencies
Installing CocoaAsyncSocket (7.3.2)
Installing CocoaHTTPServer (2.3)
Installing CocoaLumberjack (1.6.2)
Installing KIF (2.0.0)
Installing NLTHTTPStubServer (0.4.0)
Generating Pods project
Integrating client project
[!] From now on use `KIFPractice.xcworkspace`.
一旦ここでXcodeを停止し、再度Xcodeを起動する。(※xcworkspaceで起動する)
> open KIFSample.xcworkspace
ここでテスト用プロジェクト(KIFPracticeTests)に対して、さまざま設定を行います。
KIFPracticeTestsのBundle Settingsより、Bundle Loaderの値に $(BUILT_PRODUCTS_DIR)/KIFPractice.app/KIFPractice
を設定します。(KIFPracticeはアプリ名に置換してください)
次にTest Hostに $(BUNDLE_LOADER)
を設定します。
最後に、Wrapper Extention に対して、xctest
-> octest
を設定します。
これら設定に関しては、もしかしたら一旦KIFをインストールした後であれば自動的に設定されるかもです。
ここでCommand+U
を実行して、テストを実行させます。ここではXcodeが自動で作成したデフォルトのサンプルテストKIFPracticeTests.m
が実行され、エラーになります。なのでファイルごと削除してしまいます。
KIFの実行
実際にKIFのテストコードを書いていくわけですが、その前にプロダクトコードどうするって話があって、今回はとりあえずボタン押したらPage1からPage2にページ遷移しますよ、っていう簡易なコードにしてみます。コードというか、もはやStoryBoardだけで完成します。
KIFでUIコンポーネントに対する参照は、AccessibilityLabel
を用いて参照します。Page1のGo to NextボタンのAccessibility LabelにGo to Next
と設定します。
また、Page1、Page2ラベルにも同様に、Page1
, Page2
とAccessibilityLabelを設定します。
以下がテストコードです。
#import "KIFTestCase.h"
#import <KIF/KIF.h>
@interface GoToNextTests : KIFTestCase
@end
#import "GoToNextTests.h"
@implementation GoToNextTests
//クラス実行前(setup)
- (void) beforeAll {
}
//各テストメソッドの実行前
- (void) beforeEach {
}
//各テストメソッドの実行前
- (void) afterEach {
}
//クラス実行後(teardown)
- (void) afterAll {
}
//
- (void)testGotoNext
{
[tester tapViewWithAccessibilityLabel:@"Go to Next"];
// Verify that the go to next page succeeded
[tester waitForViewWithAccessibilityLabel:@"Page2"];
}
@end
beforeAll
, afterAll
, beforeEach
, afterEach
は、よくあるsetupとかteardown等と同じ役割です。
testGotoNext
が実際のテストメソッドで、Go to Nextボタンをタップして、Page2へ遷移したことを確かめるテストです。先ほど設定したAccessibilityLabelの値を利用して、ボタンをタップしたり、表示された画面を評価したりできます。
実際実行してみます。
Command+U
を押すと、シミュレータが起動し、実際にPage1からPage2へ遷移し、テストが終了します。KIFかわいいっす。
tester
という変数(実際はマクロ)は、実態はKIFUITestActorクラスで、これが主にUIをコントロールしたり評価したりする機能を持ちます。今回はボタンをタップするしかやってませんが、他にもたくさんAPIがあるのでKIFUITestActor.hを確認してみてください。
次にPage2->Page1に戻る遷移のテストを追加します。
- (void)testGotoNext
{
[tester tapViewWithAccessibilityLabel:@"Go to Next"];
// Verify that the go to next page succeeded
[tester waitForViewWithAccessibilityLabel:@"Page2"];
// back
[tester tapViewWithAccessibilityLabel:@"Back"];
// Verify that the back page succeeded
[tester waitForViewWithAccessibilityLabel:@"Page1"];
}
UINavigationBarの戻るボタンには、Back
というAccessibilityLabelが設定されているため、[tester tapViewWithAccessibilityLabel:@"Back"];
で戻るを表現できます。
Command+Uで実行し、Successするかどうか確かめます。
KIFに関してはざっとこんなところです。
以下は注意点です。
KIFの注意点
- シミュレータが事前に起動していると、実行時に怒られます。
- シミュレータ/実機ともに64bitではエラーになります。
- 実行中に結構な確率で落ちます。まだまだ不安定です。
NLTHTTPStubServer のインストール
インストールは上でやってるとおり、CocoaPodsでdoneです。
NLTHTTPStubServer の起動方法
以下でサーバインスタンスおよびスタブAPIの登録ができます。また、KIFと一緒に利用する場合はbeforeAll
に記述するとちょうどいいです。
@implementation GoToNextTests
{
NLTHTTPStubServer *server;
}
//クラス実行前(setup)
- (void) beforeAll {
// make instance
server = [NLTHTTPStubServer sharedServer];
//set stub api
NSDictionary *jsonDict = @{@"status" : @"success"};
[[[server stub] forPath:@"/api"] andJSONResponse:[NSJSONSerialization dataWithJSONObject:jsonDict options:NSJSONWritingPrettyPrinted error:nil]];
}
これだけでiOS上に12345番ポートでスタブサーバが起動するとともに、/apiというインタフェースが作成されます。
次にプロダクトコード側に、connectボタンを押した場合の挙動を書きます。
#import "ViewController2.h"
@interface ViewController2 ()
@property (nonatomic, strong) IBOutlet UILabel *status;
@end
@implementation ViewController2
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (IBAction)didSelectConnect:(id)sender
{
NSURL *api = [NSURL URLWithString:@"http://localhost:12345/api"];
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:api]
queue:[[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse *res, NSData *data, NSError *err) {
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:data options:nil error:nil];
NSString *status = [jsonDict objectForKey:@"status"];
NSAssert([status isEqualToString:@"success"], @"status invalid");
}];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
あとは、テスト時にconnectボタンをタップするように設定し、Command+U
を実行すると、テストが問題なく完了します。
通常であれば接続がNGのため、NSAssertでコケるはずですが、スタブが{status:success}を代わりに返してくれてるので継続して処理が実行されます。
こんなかんじでサーバサイドもスタブ化し、KIFを組み合わせることで、非常に楽にIntegrationTestを記述することができます。
これでObjectiveC一本で、プロダクトコードもテストコードも完結し、かつControllerを容易にテストすることができます。
次回もっと時間あるときに、KIFやNLTHTTPStubServerをもっと紹介したいと思います。