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:
と同じ部分です。
つまり、expectedArg
がnil
、observedArg
が非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 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。