![]() |
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 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。