63
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

KIFとNLTHTTPStubServerを利用して最低限のIntegrationTestを実現する

iOSのテストってぶっちゃけおざなりだったわけですが、サーバサイドはちゃんとテスト書いてるのにクライアントサイドだけ書かないとだんだん品質が不安になってくるわけです。
これまでも多少なりとも努力してきたつもりですが、iOSの場合、テスト書いといて良かったみたいなケースがこれまでまるでなくて、むしろAPIの動作確認のため書くみたいな。個人的に非常にしょぼい状況だったわけです。
ただXcode5でTest周りが強化され、かつ個人的にしっくり来るテストフレームワークの組み合わせが見つかったので、これでテンション上がってバリバリテスト書ける気がしてきたのでまとめてみました。
テンション上がらないと何にもできないっすもんね!

KIFって

あのカード決済のSquareが自社向けに作ったテストフレームワークです。
https://github.com/kif-framework/KIF

特徴としては以下です。
1. ボタンをタップした、などのUIアクションベースでテストが書けて実行できる。
2. Objective-Cでかける
3. 2.0より、Xcode5といい感じに連携して実行ができる(例 : Command+Uなど)
4. 実機でも動かせる。

この動画見たほうが早いかもですね。 (ちょっと古いですが)
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 を設定します。

Podfile
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はアプリ名に置換してください)

KIFPractice.xcodeproj_2.png

次にTest Hostに $(BUNDLE_LOADER) を設定します。

KIFPractice.xcodeproj_1.png

最後に、Wrapper Extention に対して、xctest -> octestを設定します。

KIFPractice.xcworkspace_—_KIFPractice.xcodeproj.png

これら設定に関しては、もしかしたら一旦KIFをインストールした後であれば自動的に設定されるかもです。

ここでCommand+Uを実行して、テストを実行させます。ここではXcodeが自動で作成したデフォルトのサンプルテストKIFPracticeTests.mが実行され、エラーになります。なのでファイルごと削除してしまいます。

KIFの実行

実際にKIFのテストコードを書いていくわけですが、その前にプロダクトコードどうするって話があって、今回はとりあえずボタン押したらPage1からPage2にページ遷移しますよ、っていう簡易なコードにしてみます。コードというか、もはやStoryBoardだけで完成します。

KIFPractice.xcworkspace_—_Main.storyboard.png

KIFでUIコンポーネントに対する参照は、AccessibilityLabelを用いて参照します。Page1のGo to NextボタンのAccessibility LabelにGo to Nextと設定します。
また、Page1、Page2ラベルにも同様に、Page1, Page2 とAccessibilityLabelを設定します。

KIFPractice.xcworkspace_—_Main.storyboard.png

以下がテストコードです。

GoToNextTests.h
#import "KIFTestCase.h"
#import <KIF/KIF.h>

@interface GoToNextTests : KIFTestCase

@end
GoToNextTests.m
#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に戻る遷移のテストを追加します。

GoToNextTests.m

- (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の注意点

  1. シミュレータが事前に起動していると、実行時に怒られます。
  2. シミュレータ/実機ともに64bitではエラーになります。
  3. 実行中に結構な確率で落ちます。まだまだ不安定です。

NLTHTTPStubServer のインストール

インストールは上でやってるとおり、CocoaPodsでdoneです。

NLTHTTPStubServer の起動方法

以下でサーバインスタンスおよびスタブAPIの登録ができます。また、KIFと一緒に利用する場合はbeforeAllに記述するとちょうどいいです。

GoToNextTests.m
@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ボタンを押した場合の挙動を書きます。

ViewController2
#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をもっと紹介したいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
63
Help us understand the problem. What are the problem?