2012/03/28

OCMockのobserverMockの利用法と3つの注意点

OCMockのobserverMockを利用してみたらちょっとはまってしまったので、利用法と注意点を備忘録としてまとめておきます。

なお、OCMock自体の導入や簡単な利用法については前回の記事「OCMockでテストを簡単に書く方法について」を参照してください。

observerMockの利用例

observerMockはNotificationのテストを楽にできるようにするための仕組みです。 後で失敗例をいくつか挙げますが、その前にちゃんと成功する例を見てみましょう。

実装側では何かしてからnotificationをポストしているものとします。テストではこの通知が正しいかをチェックします。

- (void)doSomething {
    // 何かする
    [[NSNotificationCenter defaultCenter] postNotificationName:kSomeChanged object:self];
}

テスト側ではOCMockObjectの+observerMockでオブザーバモックを作成して、それがnotificationを受信できるようにNotificationCenterに追加します。 このときにはOCMockで追加された-addMockObserver:name:object:を利用します。 そして、モックに期待されるnotificationを記述したら、実際にnotificationを送信させ、モックに検証させています。

- (void)testDoSomething {
    id mock = [OCMockObject observerMock];
    [[NSNotificationCenter defaultCenter] addMockObserver:mock name:kSomeChanged object:nil];

    [[mock expect] notificationWithName:kSomeChanged object:[OCMArg any]];
    [obj doSomething];

    [mock verify];
}

注意1: notification名を動的に作成しない

次のコードはテスト結果がunexpected notification observedになります (受信自体は成功だがobserverMockのverifyに失敗)。

実装側

NSString* name = [self.name stringByAppendingString:@".SomeChanged"];
[[NSNotificationCenter defaultCenter] postNotificationName:name object:self];

テスト側

obj.name = @"foobar";
[[mock expect] notificationWithName:@"foobar.SomeChanged" object:[OCMArg any]];

一番はまったのがこの注意点です。 コンソールのテスト結果のエラー表示を見るとちゃんとname = foobar.SomeChangedになっているので、かなり悩みました。

Unknown.m:0: error: -[DFSomeTest testNotification] :
OCMockObserver: unexpected notification observed:
    NSConcreteNotification 0x6b60240 {
        name = foobar.SomeChanged;
        object = <Sample: 0x6b7b770>;

いろいろ調べたところ、実行時に動的に生成した文字列だと失敗することがわかりました。

さらに調べて、observerMockのverifyではNotification名の同一性をisEqual:で判断しているためだとわかりました。 次のコードはOCMObserverRecorder.mの一部です。

    - (BOOL)matchesNotification:(NSNotification *)aNotification
    {
        return [self argument:[recordedNotification name] matchesArgument:[aNotification name]] &&
        [self argument:[recordedNotification object] matchesArgument:[aNotification object]] &&
        [self argument:[recordedNotification userInfo] matchesArgument:[aNotification userInfo]];
    }

    - (BOOL)argument:(id)expectedArg matchesArgument:(id)observedArg {
                      :
            if(([expectedArg isEqual:observedArg] == NO) &&
               !((expectedArg == nil) && (observedArg == nil)))
                    return NO;
                      :
        return NO;

対策としては、notification名を動的に作成しないようにするようにします。 前述のメソッド-argument:matchesArgument:を書きかえるのも手かもしれません。

注意2: userInfo有りのnotificationのときはテストコードのexpectでもuserInfoを設定する

次のコードはテスト結果がunexpected notification observedになります (受信自体は成功だがobserverMockのverifyに失敗)。

実装側

    [[NSNotificationCenter defaultCenter] postNotificationName:name object:self userInfo:dic];

テスト側

    [[mock expect] notificationWithName:@"foobar.CountChanged" object:[OCMArg any]]

この原因は先ほどのargument:matchesArgument:と同じ部分です。 つまり、expectedArgnilobservedArgが非nilなのでマッチしなくなっています。

なので、逆に実装側userInfoなし、テスト側userInfoありは大丈夫です。

注意3: NSNotificationQueueを利用するときはpostingStyleをNSPostNowにする

observerMockに対してNSNotificationQueueのenqueueNotification系を利用することはできます。ただし、遅延を許容する postingStyleを利用したときには、モックのverifyまでに受信機会がないことが多く、結果としてエラーになります。

次のコードはテスト結果がexpected notification was not observedになるでしょう。

実装側

    NSNotification* n = [NSNotification notificationWithName:name object:self userInfo:dic];
    [[NSNotificationQueue defaultQueue] enqueueNotification:n
                                               postingStyle:NSPostASAP
                                               coalesceMask:NSNotificationNoCoalescing
                                                   forModes:nil];

どうしてもNSPostASAPやNSPostWhenIdleにしたい場合は、タイムアウト付きverifyを利用するとよいでしょう。 これは他の遅延が生じるテストでも同様です。

なお、タイムアウト付きverifyについては次のStackOverflowの記事の+waitForVerifiedMock:delayを参考にしてください。

まとめ

OCMockのobserverMockの簡単な利用法の紹介と注意すべき3点について述べました。

関連情報

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。