連載
» 2014年04月04日 18時00分 公開

Kiwi+CocoaPodsで始めるiOSアプリの振る舞いテスト入門iOSアプリ開発でもCI/継続的デリバリしようぜ(2)(4/4 ページ)

[諏訪悠紀,アンダースコア]
前のページへ 1|2|3|4       

例外のテスト

 特定の処理内で例外が発生したかテストするマッチャーも用意されています。次のように、ブロック内で例外が発生したかどうかをテストできます。

[[theBlock(^{
    // エラーを発生させる
    [NSException raise:@"SampleException" reason:@“ERROR-001”];
}) should] raiseWithName:@"SampleException" reason:@"ERROR-001"];

 例外のテストで使用できるマッチャーは次の通りです。

マッチャー 説明
[[theBlock(^{ ... }) should] raise] ブロック内で例外が発生したか
[[theBlock(^{ ... }) should] raiseWithName:] ブロック内で指定した名前の例外が発生したか
[[theBlock(^{ ... }) should] raiseWithReason:(NSString *)aReason] ブロック内で指定した理由の例外が発生したか
[[theBlock(^{ ... }) should] raiseWithName:(NSString *)aName reason:(NSString *)aReason] ブロック内で指定した名前・理由の例外が発生したか

カスタムマッチャー

 ここまで、さまざまなマッチャーを見てきましたが、iOSアプリケーションの要件によってはカスタムのマッチャーを作らなければいけない場合があります。そういった場合を想定し、Kiwiではカスタムのマッチャーを作成できます。

 そこで、今回はNSDateを比較するシンプルなマッチャーを作ってみたいと思います。

 まずKWMatcherクラスを継承したクラス「KWSDateMatcher」を新規に作成してください。KWMatcherクラスはマッチャーの最低限の動作を定義した、NSObjectクラスのカテゴリクラスです。クラスは次のように実装してください。

#import "KWMatcher.h"
@interface KWSDateMatcher : KWMatcher
@property (nonatomic, readwrite) NSInteger otherSubject;
- (void)equalMonth:(NSInteger)anObject;
@end
KWSDateMatcher.h
#import "KWSDateMatcher.h"
@implementation KWSDateMatcher
// マッチャーのメソッド名を登録する
+ (NSArray *)matcherStrings {
    return [NSArray arrayWithObjects:@"equalMonth:", nil];
}
// 評価の処理を実装する
- (BOOL)evaluate {
    NSCalendar* calendar = [NSCalendar currentCalendar];
    NSDateComponents* components = [calendar components:NSMonthCalendarUnit fromDate:self.subject];
    NSInteger expect = (int)components.month;
    return expect == self.otherSubject;
}
// shoudマクロの場合のテスト失敗時のメッセージ
- (NSString *)failureMessageForShould {
    return @"期待値と異なる";
}
// shoudNotマクロの場合のテスト失敗時のメッセージ
- (NSString *)failureMessageForShouldNot {
    return @"期待値と同一";
}
// マッチャーのメソッド
- (void)equalMonth:(NSInteger)anObject {
    self.otherSubject = anObject;
}
@end
KWSDateMatcher.m

 順番に確認していきましょう。まずmatcherStringsメソッドでこのマッチャークラスで使えるメソッドを配列で指定します。次のevaluateメソッドは実際の比較処理を行うメソッドで、テストケースが実行され比較するときに呼び出されます。ここではNSDateの月を比較している実装になっています。次のfailureMessageForShouldメソッドとfailureMessageForShouldNotメソッドは、評価に失敗したときにエラーとして表示する文字列を返すようにします。最後のequalMonth:メソッドがカスタムのマッチャーメソッドになります。

 このマッチャークラスをテストケースで使用するには、テストケースでregisterMatchersメソッドを使います。このメソッドは引数に渡されたプレフィックスのクラスをマッチャーとしてKiwiに登録するメソッドです。次のようになります。

#import "Kiwi.h"
#import "KWSDateMatcher.h"
SPEC_BEGIN(NSDateSpec)
registerMatchers(@"KWS");
describe(@"NSDate", ^{
    it(@"3月であること", ^{
        NSDate *date = [NSDate new];
        [[date should] equalMonth:3];
    });
});
SPEC_END

 このように、既存のマッチャーで物足りない場合はカスタムのマッチャークラスを簡単・自由に追加できます。

オブジェクトを疑似的に再現する「モック」とメソッドの戻り値を疑似的に再現する「スタブ」

 テストケースを書いていると、ある条件におけるオブジェクトを再現する際、オブジェクトやプロパティがどうしても再現できなかったり、再現するために必要な処理が多くて面倒なことがあります。このような再現が困難なオブジェクトを疑似的に再現する仕組みがモックとスタブです。

 「モック」はオブジェクトを疑似的に再現したい場合に使い、「スタブ」はメソッドの戻り値を疑似的に再現したい場合に使います。

モックの作り方

 モックは対象のクラスのmockメソッドか、KWMockクラスのmockForClassメソッドを使って生成します。これだけでモックオブジェクトは「対象のクラスから生成したオブジェクト」として取り扱えます。

 下記の例では、Carクラスのmockオブジェクトを生成しています。モックオブジェクトのクラスがCarクラスであるかという確認と、currentGearメソッドの戻り値の確認をしています。

// モックの生成
id carMock = [Car mock];
// クラスの比較の確認
[[carMock should] beMemberOfClass:[Car class]];
// 呼び出される予定のメソッドの戻り値の指定
[[carMock should] receive:@selector(currentGear) andReturn:theValue(3)];
// メソッドの呼び出し確認
[[theValue(carMock.currentGear) should] equal:theValue(3)];

 上記の例のように生成したモックでは、receiveメソッドで指定されていないメソッドが1つでも呼び出されると例外が出力されるため、呼び出されるメソッドの全てについて、receiveメソッドを指定しなければいけません。

 このような問題を解決するため、Kiwiではクラスのメソッドを全て自動で追加してくれる「NullMock」と呼ばれるモックオブジェクトを生成できます。NullMockは対象のクラスのnullMockメソッドか、KWMockクラスのnullMockForClassメソッドを使って生成します。モックの戻り値を考慮する必要がない場合や、例外を発生させたくない場合に便利です。

// NullMockの生成
id carNullMock = [Car nullMock];
// メソッドの戻り値の確認
[[theValue(carNullMock.currentGear) should] equal:theValue(0)];
// メソッドの呼び出しの確認
[carNullMock applyBrakes];

 Kiwiでは、さらにもう1つProtocolMockを生成する機能もあります。この機能を使うと、Delegateのモックなどを生成できます。ProtocolMockはKWMockのmockForProtocolメソッドを使って生成します。

// ProtocolMockの生成
id flyerMock = [KWMock mockForProtocol:@protocol(FlyingMachine)];
// FlyingMachineプロトコルに準拠しているか確認する
[[flyerMock should] conformToProtocol:@protocol(FlyingMachine)];

スタブの作り方

 オブジェクトのメソッドをスタブのメソッドに差し替えるには、スタブを適用したいオブジェクトのstubメソッド、またはstub:andReturnメソッドを使います。引数には差し替えたいメソッドのセレクターを渡します。

id cruiser = [Cruiser cruiser];
// energyLevelInWarpCoreメソッドをスタブに差し替える
[[cruiser stubAndReturn:theValue(42.0f)] energyLevelInWarpCore:7];
// energyLevelInWarpCoreメソッドの戻り値の確認
float energyLevel = [cruiser energyLevelInWarpCore:7];
[[theValue(energyLevel) should] equal:theValue(42.0f)];

 なお、オブジェクトに適用したスタブは、itメソッドのブロックの最後に解除されます。

継続的にテストする習慣を付けよう

 本稿では、機能の振る舞いをテストするためのテスティングフレームワークであるKiwiの導入方法と使い方を解説しました。テストケースをコンテキストによって階層構造で表現できるので、分かりやすく無駄の無いテストケースが作成できると思います。

 テストは一度実行しただけでは意味はありません。日々繰り返し、継続することでプロジェクトに多くの価値をもたらします。

 不慣れなうちは、まずは比較的テストしやすい部分(ユーティリティクラスなど)からテストケースを書き始めるといいと思います。そこから徐々に慣れていき、テストする範囲を増やしていければいいでしょう。実装を進めながらテストケースを書いて確認する習慣が楽しくなってくるはずです。

著者プロフィール

諏訪悠紀

アンダースコア株式会社でiOS/Androidのアプリの開発に携わっています。


ブログGoogle Play StoreiTunes App Store


前のページへ 1|2|3|4       

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。